sable-sphynx 0.7.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. sphynx/workflows.py +792 -0
@@ -0,0 +1,393 @@
1
+ """Grade frozen score fields against separate labels; never fit or infer.
2
+
3
+ Probability, rank utility and pool-relative diagnostics are distinct contracts.
4
+ Local lineage is caller-declared and does not grant benchmark/reviewer authority.
5
+ The mathematical helpers port the pinned nextgen/calibrator source functions.
6
+ """
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass
9
+ from collections.abc import Mapping
10
+ from pathlib import Path
11
+ import math
12
+ from .contracts import ContractError,canonical_json
13
+ from .integration_contracts import require_digest,value_digest
14
+ from ._track_capture import strict_json,byte_digest
15
+ from .rerank_family_contracts import SOURCE_PINS
16
+
17
+ REFERENCE_SCHEMA="sphynx.score-reliability.references.v1"
18
+ REPORT_SCHEMA="sphynx.score-reliability.result.v1"
19
+ POLICIES=frozenset({"fully_labeled","known_judgments_only","positive_unlabeled_diagnostic"})
20
+ TARGETS={"frozen_strong_relevance":("sable_0_to_3",2),"native_positive_qrel":("native_public_qrels",1)}
21
+
22
+
23
+ def _require(condition,message):
24
+ if not condition:raise ContractError(message)
25
+
26
+
27
+ def _number(value):
28
+ try:return type(value) in (int,float) and math.isfinite(value)
29
+ except OverflowError:return False
30
+
31
+
32
+ def _plain(value):
33
+ if isinstance(value,Mapping):value={key:_plain(child) for key,child in value.items()}
34
+ elif isinstance(value,(tuple,list)):value=[_plain(child) for child in value]
35
+ return strict_json(canonical_json(value))
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ReliabilityReference:
40
+ """Exact held label release, inputs/corpus/cohort and declared label policy."""
41
+ _value_bytes:bytes
42
+
43
+ def __post_init__(self):
44
+ raw=strict_json(self._value_bytes)
45
+ fields={"schema_version","input_digest","corpus_digest","label_release","label_revision","label_policy","grade_scale",
46
+ "probability_target","authority","lineage","cases"}
47
+ _require(type(raw) is dict and set(raw)==fields and raw["schema_version"]==REFERENCE_SCHEMA,"reliability reference fields differ")
48
+ for field in ("input_digest","corpus_digest"):require_digest(raw[field],field)
49
+ _require(type(raw["label_release"]) is str and raw["label_release"] and type(raw["label_revision"]) is str and raw["label_revision"],"label release/revision is required")
50
+ _require(type(raw["label_policy"]) is str and type(raw["probability_target"]) is str
51
+ and raw["label_policy"] in POLICIES and raw["probability_target"] in TARGETS
52
+ and raw["grade_scale"]==TARGETS[raw["probability_target"]][0],"label semantics and probability target differ")
53
+ _require(raw["authority"] in ("synthetic_fixture","caller_declared_unverified","native_public_qrels"),"local reference authority cannot claim verified review")
54
+ lineage=raw["lineage"]
55
+ _require(type(lineage) is dict and set(lineage)=={"parent_reference_digest","relationship","changed_case_count","evidence_digest"},"label lineage fields differ")
56
+ for field in ("parent_reference_digest","evidence_digest"):
57
+ if lineage[field] is not None:require_digest(lineage[field],field)
58
+ _require(lineage["relationship"] in ("root","label_revision","subset") and type(lineage["changed_case_count"]) is int
59
+ and lineage["changed_case_count"]>=0,"label lineage relationship/count differs")
60
+ _require((lineage["relationship"]=="root")== (lineage["parent_reference_digest"] is None)
61
+ and (lineage["changed_case_count"]==0 or lineage["evidence_digest"] is not None),"changed label lineage needs evidence")
62
+ _require(lineage["relationship"]!="root" or lineage["changed_case_count"]==0,"root label release cannot claim changed parent labels")
63
+ cases=raw["cases"];_require(type(cases) is list and 1<=len(cases)<=10000,"reliability reference cohort is empty or too large")
64
+ ids=set();total=0
65
+ for row in cases:
66
+ _require(type(row) is dict and set(row)=={"query_id","cluster_id","input_digest","candidate_ids","dense_ranks","grades","judgment_status"},"reference case fields differ")
67
+ _require(type(row["query_id"]) is str and row["query_id"] and row["query_id"] not in ids
68
+ and type(row["cluster_id"]) is str and row["cluster_id"],"query/cluster identity differs")
69
+ ids.add(row["query_id"]);require_digest(row["input_digest"],"case input_digest")
70
+ candidates=row["candidate_ids"]
71
+ _require(type(candidates) is list and 1<=len(candidates)<=100 and all(type(cid) is str and cid for cid in candidates)
72
+ and len(candidates)==len(set(candidates)),"candidate IDs differ")
73
+ n=len(candidates);total+=n
74
+ _require(type(row["dense_ranks"]) is list and all(type(rank) is int for rank in row["dense_ranks"])
75
+ and sorted(row["dense_ranks"])==list(range(1,n+1)),"dense rank identity differs")
76
+ _require(type(row["grades"]) is list and type(row["judgment_status"]) is list
77
+ and len(row["grades"])==n and len(row["judgment_status"])==n,"reference label alignment differs")
78
+ for grade,status in zip(row["grades"],row["judgment_status"]):
79
+ _require(status in ("judged","unknown") and ((status=="unknown" and grade is None)
80
+ or (status=="judged" and type(grade) is int and 0<=grade<=3)),"unknown labels cannot imply negatives")
81
+ _require(total<=1000000 and lineage["changed_case_count"]<=len(cases),"reference cohort/count exceeds bounds")
82
+ object.__setattr__(self,"_value_bytes",canonical_json(raw).encode())
83
+
84
+ @property
85
+ def digest(self):return byte_digest(self._value_bytes)
86
+
87
+ def to_dict(self):return strict_json(self._value_bytes)
88
+
89
+
90
+ def validate_reliability_reference(value):return ReliabilityReference(canonical_json(_plain(value)).encode())
91
+
92
+
93
+ def load_reliability_reference(path,*,expected_sha256):
94
+ raw=Path(path).read_bytes();_require(byte_digest(raw)==expected_sha256,"reference artifact bytes differ")
95
+ return ReliabilityReference(raw)
96
+
97
+
98
+ def _mean(values):return sum(values)/len(values) if values else 0.0
99
+
100
+
101
+ def _percentile(values,fraction):
102
+ if not values:return None
103
+ ordered=sorted(values);position=fraction*(len(ordered)-1);lower=math.floor(position);upper=math.ceil(position)
104
+ return ordered[lower]+(ordered[upper]-ordered[lower])*(position-lower)
105
+
106
+
107
+ def _bins(probabilities,labels,bin_count=10,grades=None):
108
+ bins=[]
109
+ for index in range(bin_count):
110
+ lower=index/bin_count;upper=(index+1)/bin_count
111
+ selected=[i for i,p in enumerate(probabilities) if p>=lower and (p<=upper if index==bin_count-1 else p<upper)]
112
+ row={"lower":lower,"upper":upper,"count":len(selected),
113
+ "mean_probability":_mean([probabilities[i] for i in selected]) if selected else None,
114
+ "positive_rate":_mean([labels[i] for i in selected]) if selected else None}
115
+ if grades is not None:row["relevance_grade_distribution"]={str(g):sum(grades[i]==g for i in selected) for g in sorted({grades[i] for i in selected if grades[i] is not None})}
116
+ bins.append(row)
117
+ return bins
118
+
119
+
120
+ def _log_loss(probability,label):
121
+ p=min(1-1e-12,max(1e-12,probability))
122
+ return -(label*math.log(p)+(1-label)*math.log(1-p))
123
+
124
+
125
+ def _probability_metrics(queries,interpretation):
126
+ """Source probabilityMetrics math over grader-created labeled rows only."""
127
+ all_rows=[row for query in queries for row in query["rows"]]
128
+ top=[row["probability"] for query in queries for row in sorted(query["rows"],key=lambda row:(-row["score"],row["dense_rank"],row["candidate_id"]))[:5]]
129
+ p=[row["probability"] for row in all_rows];y=[row["label"] for row in all_rows];widths=[row["width"] for row in all_rows if row["width"]>0]
130
+ saturated=lambda value:value<=.05 or value>=.95
131
+ return {"interpretation":interpretation,"candidate_count":len(p),"positive_count":sum(y),
132
+ "brier_score":_mean([(v-label)**2 for v,label in zip(p,y)]) if p else None,
133
+ "log_loss":_mean([_log_loss(v,label) for v,label in zip(p,y)]) if p else None,
134
+ "saturation_fraction":sum(saturated(v) for v in p)/len(p) if p else 0,
135
+ "top_five_saturation_fraction":sum(saturated(v) for v in top)/len(top) if top else 0,
136
+ "probability_quantiles":{"p05":_percentile(p,.05),"p50":_percentile(p,.5),"p95":_percentile(p,.95)},
137
+ "mean_venn_abers_interval_width":_mean(widths) if widths else None,"p95_venn_abers_interval_width":_percentile(widths,.95),
138
+ "reliability":_bins(p,y)}
139
+
140
+
141
+ def _score_only_diagnostics(queries):
142
+ """Score distributions need no labels and retain the original ranked pool."""
143
+ rows=[row for query in queries for row in query["rows"]]
144
+ probabilities=[row["probability"] for row in rows]
145
+ top=[row["probability"] for query in queries for row in sorted(query["rows"],
146
+ key=lambda row:(-row["score"],row["dense_rank"],row["candidate_id"]))[:5]]
147
+ widths=[row["width"] for row in rows if row["width"]>0]
148
+ saturated=lambda value:value<=.05 or value>=.95
149
+ return {"population":"all_captured_candidates_original_top_five","candidate_count":len(rows),
150
+ "top_five_candidate_count":len(top),
151
+ "saturation_fraction":sum(saturated(p) for p in probabilities)/len(probabilities) if probabilities else 0,
152
+ "top_five_saturation_fraction":sum(saturated(p) for p in top)/len(top) if top else 0,
153
+ "probability_quantiles":{"p05":_percentile(probabilities,.05),"p50":_percentile(probabilities,.5),"p95":_percentile(probabilities,.95)},
154
+ "mean_venn_abers_interval_width":_mean(widths) if widths else None,"p95_venn_abers_interval_width":_percentile(widths,.95)}
155
+
156
+
157
+ def _calibration_summary(probabilities,labels):
158
+ if not probabilities:return {"brier_score":None,"log_loss":None,"expected_calibration_error":None,"max_reliability_bin_error":None,"reliability":_bins([],[])}
159
+ bins=_bins(probabilities,labels)
160
+ errors=[(row["count"]/len(labels),abs(row["mean_probability"]-row["positive_rate"])) for row in bins if row["count"]]
161
+ return {"brier_score":_mean([(p-y)**2 for p,y in zip(probabilities,labels)]),
162
+ "log_loss":_mean([_log_loss(p,y) for p,y in zip(probabilities,labels)]),
163
+ "expected_calibration_error":sum(weight*error for weight,error in errors),
164
+ "max_reliability_bin_error":max(error for _,error in errors),"reliability":bins}
165
+
166
+
167
+ def grade_score_reliability(score_queries,references,*,field="evidence_probability",bootstrap_samples=1000,seed=0):
168
+ """Grade one typed score field; held references never enter the scorer.
169
+
170
+ score_queries contain query_id/cluster_id/input_digest/scores (the family
171
+ score envelope). Any probability target mismatch fails closed. Diagnostic
172
+ fields can be analyzed but cannot yield an admitted probability result.
173
+ """
174
+ _require(isinstance(references,ReliabilityReference),"validated held references are required")
175
+ _require(type(field) is str and field,"a named typed score field is required")
176
+ _bootstrap_parameters(bootstrap_samples,seed)
177
+ ref=references.to_dict();queries=_plain(score_queries)
178
+ _require(type(queries) is list and len(queries)==len(ref["cases"]),"score/reference query cohort differs")
179
+ refs={row["query_id"]:row for row in ref["cases"]};seen=set();graded=[];full_pool=[];all_unknown=0;kind=None;method=None
180
+ for query in queries:
181
+ _require(type(query) is dict and set(query)=={"query_id","cluster_id","input_digest","scores"},"score query fields differ")
182
+ _require(type(query["query_id"]) is str,"score query ID must be text")
183
+ case=refs.get(query["query_id"])
184
+ _require(case is not None and query["query_id"] not in seen and query["cluster_id"]==case["cluster_id"]
185
+ and query["input_digest"]==case["input_digest"],"score query/cluster/input identity differs")
186
+ seen.add(query["query_id"]);scores=query["scores"]
187
+ required={"schema_version","method","method_version","candidate_ids","fields","diagnostics","reference_sources","inference_observed"}
188
+ _require(type(scores) is dict and set(scores)==required and scores["schema_version"]=="sphynx.rerank-family.scores.v1"
189
+ and scores["candidate_ids"]==case["candidate_ids"] and type(scores["fields"]) is dict
190
+ and type(scores["method"]) is str and scores["method"] and type(scores["method_version"]) is str
191
+ and type(scores["inference_observed"]) is bool and type(scores["diagnostics"]) is dict
192
+ and type(scores["reference_sources"]) is dict,"typed score envelope differs")
193
+ for source,digest in scores["reference_sources"].items():
194
+ _require(type(source) is str and source,"score source identities differ");require_digest(digest,"score source digest")
195
+ current=(scores["method"],scores["method_version"])
196
+ _require(method is None or current==method,"methods differ inside one score cohort");method=current
197
+ utility=scores["fields"].get("ranking_utility");selected=scores["fields"].get(field)
198
+ _require(type(utility) is dict and set(utility)=={"kind","values"} and utility["kind"]=="utility"
199
+ and type(selected) is dict and set(selected) in ({"kind","values"},{"kind","values","probability_target"}),"selected score fields differ")
200
+ _require(type(selected["kind"]) is str and selected["kind"] in ("probability","diagnostic","pool_relative","utility")
201
+ and (kind is None or kind==selected["kind"]),"score field kinds differ");kind=selected["kind"]
202
+ n=len(case["candidate_ids"])
203
+ for values in (utility["values"],selected["values"]):
204
+ _require(type(values) is list and len(values)==n and all(_number(v) for v in values),"score field alignment/numbers differ")
205
+ _require(all(0<=v<=1 for v in selected["values"]),"reliability diagnostics require a bounded 0..1 field, not arbitrary rank utility")
206
+ target=selected.get("probability_target")
207
+ _require(kind!="probability" or target==ref["probability_target"],"probability score target differs from the held label target")
208
+ _require(kind=="probability" or target is None,"diagnostic score field cannot claim a probability target")
209
+ width=scores["fields"].get("venn_abers_interval_width",{"values":[0.0]*n,"kind":"diagnostic"})
210
+ _require(type(width) is dict and set(width)=={"kind","values"} and width["kind"]=="diagnostic"
211
+ and type(width["values"]) is list and len(width["values"])==n and all(_number(v) and 0<=v<=1 for v in width["values"]),"interval width diagnostics differ")
212
+ full_pool.append({"rows":[{"candidate_id":case["candidate_ids"][i],"dense_rank":case["dense_ranks"][i],
213
+ "score":utility["values"][i],"probability":selected["values"][i],"width":width["values"][i]} for i in range(n)]})
214
+ rows=[];unknown=0
215
+ for index,grade in enumerate(case["grades"]):
216
+ absent=grade is None;unknown+=absent
217
+ if absent and ref["label_policy"]!="positive_unlabeled_diagnostic":continue
218
+ label=0 if absent else int(grade>=TARGETS[ref["probability_target"]][1])
219
+ rows.append({"candidate_id":case["candidate_ids"][index],"dense_rank":case["dense_ranks"][index],
220
+ "score":utility["values"][index],"probability":selected["values"][index],"width":width["values"][index],"label":label})
221
+ all_unknown+=unknown;graded.append({"query_id":case["query_id"],"cluster_id":case["cluster_id"],"rows":rows,"unknown_count":unknown})
222
+ policy=ref["label_policy"]
223
+ interpretation="diagnostic_unjudged_as_negative" if policy=="positive_unlabeled_diagnostic" else "formal_fully_labeled" if not all_unknown else "conditional_known_judgments"
224
+ source=_probability_metrics(graded,interpretation)
225
+ p=[row["probability"] for query in graded for row in query["rows"]];labels=[row["label"] for query in graded for row in query["rows"]]
226
+ micro=_calibration_summary(p,labels)
227
+ per_query=[{"query_id":query["query_id"],"candidate_count":len(query["rows"]),"unknown_count":query["unknown_count"],
228
+ **_calibration_summary([row["probability"] for row in query["rows"]],[row["label"] for row in query["rows"]])} for query in graded]
229
+ macro={key:_mean([row[key] for row in per_query if row[key] is not None]) if any(row[key] is not None for row in per_query) else None
230
+ for key in ("brier_score","log_loss","expected_calibration_error","max_reliability_bin_error")}
231
+ eligible=kind=="probability" and policy!="positive_unlabeled_diagnostic" and (policy!="fully_labeled" or not all_unknown) and bool(p)
232
+ blockers=[]
233
+ if kind!="probability":blockers.append("field_is_not_an_admitted_probability")
234
+ if policy=="positive_unlabeled_diagnostic":blockers.append("positive_unlabeled_metrics_are_diagnostic")
235
+ if policy=="fully_labeled" and all_unknown:blockers.append("required_judgments_missing")
236
+ if not p:blockers.append("no_judged_candidates")
237
+ report={"schema_version":REPORT_SCHEMA,"method":method[0],"method_version":method[1],"field":field,"field_kind":kind,
238
+ "probability_target":ref["probability_target"],"label_policy":policy,"reference_digest":references.digest,
239
+ "score_capture_digest":value_digest(queries),"input_digest":ref["input_digest"],"corpus_digest":ref["corpus_digest"],
240
+ "label_release":ref["label_release"],"label_revision":ref["label_revision"],"lineage":ref["lineage"],
241
+ "authority":ref["authority"],"semantic_authority_verified":False,"inference_executed":False,
242
+ "query_count":len(graded),"candidate_count":sum(len(row["candidate_ids"]) for row in ref["cases"]),
243
+ "judged_count":sum(len(row["candidate_ids"]) for row in ref["cases"])-all_unknown,"unknown_count":all_unknown,
244
+ "formal_probability_eligible":eligible,"status":"eligible_conditional" if eligible and all_unknown else "eligible" if eligible else "diagnostic_or_blocked",
245
+ "blockers":blockers,"formal_passage_micro":micro if eligible else None,"formal_query_macro":macro if eligible else None,
246
+ "diagnostics":{"source_probability_metrics":source,
247
+ "source_probability_metrics_population":"all_candidates_explicit_unjudged_as_negative" if policy=="positive_unlabeled_diagnostic"
248
+ else "judged_subset_reranked_top_five" if all_unknown else "all_fully_judged_candidates_original_top_five",
249
+ "full_pool_score_only":_score_only_diagnostics(full_pool),
250
+ "passage_micro":micro,"query_macro":macro,"per_query":per_query},
251
+ "bootstrap":None,"notice":"Frozen score analysis only. Smaller intervals or saturation alone do not establish calibration. Known-only scores apply to the judged subset.",
252
+ "source_pins":{key:SOURCE_PINS[key] for key in ("nextgen-evaluation.ts","calibrator-fit.ts","metrics.ts")}}
253
+ report["scoring_protocol"]={"version":"source-score-reliability.v1","reliability_bin_count":10,
254
+ "log_loss_epsilon":1e-12,"saturation_low":.05,"saturation_high":.95,"saturation_top_k":5,
255
+ "interval_width_population":"strictly_positive_widths","label_threshold":TARGETS[ref["probability_target"]][1]}
256
+ report["declared_scorer_sources"]=[query["scores"]["reference_sources"] for query in queries]
257
+ if eligible:
258
+ clusters={row["query_id"]:row["cluster_id"] for row in graded}
259
+ values=[{"cluster":clusters[row["query_id"]],"value":row["brier_score"]} for row in per_query if row["brier_score"] is not None]
260
+ report["bootstrap"]=_cluster_bootstrap(values,bootstrap_samples,seed)
261
+ report["bootstrap"].update(estimate="query_macro_brier",reference_digest=references.digest,
262
+ score_capture_digest=report["score_capture_digest"],conditional_on_judged_subset=bool(all_unknown))
263
+ return report
264
+
265
+
266
+ def _bootstrap_parameters(samples,seed):
267
+ _require(type(samples) is int and 1<=samples<=10000 and type(seed) is int and 0<=seed<=0xffffffff,
268
+ "bootstrap configuration must use bounded repetitions and a UInt32 seed")
269
+
270
+
271
+ def _lcg32(seed):
272
+ state=seed
273
+ while True:
274
+ state=(1664525*state+1013904223)&0xffffffff
275
+ yield state/4294967296
276
+
277
+
278
+ def _cluster_bootstrap(rows,samples,seed):
279
+ _bootstrap_parameters(samples,seed)
280
+ clusters=sorted({row["cluster"] for row in rows})
281
+ _require(len(rows)*samples<=10000000,"cluster bootstrap exceeds bounded work")
282
+ if not rows:return {"sampling_unit":"cluster_id","rng":"nextgen-evaluation.lcg32.v1","seed":seed,
283
+ "repetitions":samples,"independent_cluster_count":0,"observed_query_count":0,"interval_95":None,"standard_deviation":None}
284
+ grouped={cluster:[row["value"] for row in rows if row["cluster"]==cluster] for cluster in clusters}
285
+ random=_lcg32(seed);means=[]
286
+ for _ in range(samples):
287
+ drawn=[clusters[int(next(random)*len(clusters))] for _ in clusters]
288
+ means.append(_mean([value for cluster in drawn for value in grouped[cluster]]))
289
+ center=_mean(means)
290
+ return {"sampling_unit":"cluster_id","rng":"nextgen-evaluation.lcg32.v1","seed":seed,"repetitions":samples,
291
+ "independent_cluster_count":len(clusters),"observed_query_count":len(rows),
292
+ "interval_95":{"lower":_percentile(means,.025),"upper":_percentile(means,.975)},
293
+ "standard_deviation":math.sqrt(_mean([(value-center)**2 for value in means]))}
294
+
295
+
296
+ def compare_score_reliability(baseline_queries,candidate_queries,references,*,baseline_field="evidence_probability",
297
+ candidate_field="evidence_probability",bootstrap_samples=1000,seed=0):
298
+ """Paired query Brier improvement with matched labels/inputs and clusters."""
299
+ left=grade_score_reliability(baseline_queries,references,field=baseline_field,bootstrap_samples=bootstrap_samples,seed=seed)
300
+ right=grade_score_reliability(candidate_queries,references,field=candidate_field,bootstrap_samples=bootstrap_samples,seed=seed)
301
+ _require(left["formal_probability_eligible"] and right["formal_probability_eligible"],"diagnostic/missing probability scores cannot enter a formal comparison")
302
+ a={row["query_id"]:row for row in left["diagnostics"]["per_query"]};b={row["query_id"]:row for row in right["diagnostics"]["per_query"]}
303
+ clusters={row["query_id"]:row["cluster_id"] for row in references.to_dict()["cases"]}
304
+ deltas=[{"cluster":clusters[qid],"value":a[qid]["brier_score"]-b[qid]["brier_score"]} for qid in a
305
+ if a[qid]["brier_score"] is not None and b[qid]["brier_score"] is not None]
306
+ return {"schema_version":"sphynx.score-reliability.comparison.v1","reference_digest":references.digest,
307
+ "baseline_score_capture_digest":left["score_capture_digest"],"candidate_score_capture_digest":right["score_capture_digest"],
308
+ "label_policy":left["label_policy"],"aggregation":"query_macro","metric":"brier_score",
309
+ "direction":"baseline_minus_candidate_positive_means_lower_candidate_error",
310
+ "mean_improvement":_mean([row["value"] for row in deltas]) if deltas else None,
311
+ "bootstrap":_cluster_bootstrap(deltas,bootstrap_samples,seed),"inference_executed":False,
312
+ "conditional_on_judged_subset":bool(left["unknown_count"])}
313
+
314
+
315
+ def _mulberry32(seed):
316
+ state=seed&0xffffffff
317
+ while True:
318
+ state=(state+0x6d2b79f5)&0xffffffff
319
+ value=((state^(state>>15))*(state|1))&0xffffffff
320
+ value^=(value+((value^(value>>7))*(value|61)))&0xffffffff
321
+ yield ((value^(value>>14))&0xffffffff)/4294967296
322
+
323
+
324
+ def _sigmoid(value):
325
+ return 1/(1+math.exp(-value)) if value>=0 else math.exp(value)/(1+math.exp(value))
326
+
327
+
328
+ def _apply_calibrator(calibrator,value):
329
+ method=calibrator["method"]
330
+ if method=="temperature":return _sigmoid(value/calibrator["temperature"])
331
+ if method=="platt":return _sigmoid(calibrator["a"]*value+calibrator["b"])
332
+ if method=="beta":
333
+ p=min(1-1e-12,max(1e-12,_sigmoid(value)))
334
+ return _sigmoid(calibrator["a"]*math.log(p)-calibrator["b"]*math.log(1-p)+calibrator["c"])
335
+ index=next((i for i,upper in enumerate(calibrator["upper_bounds"]) if value<=upper),len(calibrator["probabilities"])-1)
336
+ return calibrator["probabilities"][index]
337
+
338
+
339
+ def evaluate_fitted_calibrator(observations,calibrator,*,bin_count=10,bootstrap_samples=1000,seed=0):
340
+ """Exact source evaluator math for a fixed calibrator. No fitting/selection.
341
+
342
+ Observations are grader-only held raw-logit/label records. This math helper
343
+ cannot assert admission, cohort independence, or fitted-artifact provenance.
344
+ """
345
+ observations=_plain(observations);calibrator=_plain(calibrator)
346
+ _require(type(observations) is list and 1<=len(observations)<=10000,"calibrator observations differ")
347
+ _require(type(bin_count) is int and 1<=bin_count<=100 and type(bootstrap_samples) is int
348
+ and 1<=bootstrap_samples<=10000 and len(observations)*bootstrap_samples<=10000000
349
+ and type(seed) is int and 0<=seed<=0xffffffff,"bootstrap/bin configuration exceeds bounds")
350
+ fields={"temperature":{"method","temperature"},"platt":{"method","a","b"},"beta":{"method","a","b","c"},
351
+ "isotonic":{"method","upper_bounds","probabilities"}}
352
+ _require(type(calibrator) is dict and type(calibrator.get("method")) is str
353
+ and calibrator["method"] in fields and set(calibrator)==fields[calibrator["method"]],"fixed calibrator fields differ")
354
+ method=calibrator["method"]
355
+ if method=="isotonic":
356
+ bounds=calibrator["upper_bounds"];values=calibrator["probabilities"]
357
+ _require(type(bounds) is list and type(values) is list and bounds and len(bounds)==len(values)
358
+ and all(_number(v) for v in bounds) and all(a<b for a,b in zip(bounds,bounds[1:]))
359
+ and all(_number(v) and 0<=v<=1 for v in values) and values==sorted(values),"fixed isotonic knots differ")
360
+ else:_require(all(_number(v) for key,v in calibrator.items() if key!="method")
361
+ and (method!="temperature" or calibrator["temperature"]>0),"fixed calibrator coefficients differ")
362
+ probabilities=[];labels=[];grades=[]
363
+ for row in observations:
364
+ _require(type(row) is dict and {"query_id","category","raw_logit","label"}<=set(row)
365
+ and set(row)<={"query_id","category","raw_logit","label","relevance_grade","uncertainty"}
366
+ and type(row["query_id"]) is str and row["query_id"] and type(row["category"]) is str and row["category"]
367
+ and _number(row["raw_logit"]) and type(row["label"]) is int and row["label"] in (0,1),"calibrator held observation differs")
368
+ grade=row.get("relevance_grade");_require(grade is None or type(grade) is int and 0<=grade<=3,"relevance grade differs")
369
+ probability=_apply_calibrator(calibrator,row["raw_logit"])
370
+ _require(_number(probability) and 0<=probability<=1,"fixed calibrator produced an invalid probability")
371
+ probabilities.append(probability);labels.append(row["label"]);grades.append(grade)
372
+ bins=_bins(probabilities,labels,bin_count,grades)
373
+ errors=[abs(row["mean_probability"]-row["positive_rate"]) for row in bins if row["count"]]
374
+ random=_mulberry32(seed);bootstrap=[]
375
+ for _ in range(bootstrap_samples):
376
+ bootstrap.append(_mean([(probabilities[i]-labels[i])**2 for i in (int(next(random)*len(labels)) for _ in labels)]))
377
+ bootstrap_mean=_mean(bootstrap)
378
+ return {"method":method,"brier_score":_mean([(p-y)**2 for p,y in zip(probabilities,labels)]),
379
+ "log_loss":_mean([_log_loss(p,y) for p,y in zip(probabilities,labels)]),
380
+ "expected_calibration_error":sum(row["count"]/len(labels)*abs(row["mean_probability"]-row["positive_rate"]) for row in bins if row["count"]),
381
+ "max_reliability_bin_error":max([0,*errors]),"bootstrap_brier_standard_deviation":math.sqrt(_mean([(value-bootstrap_mean)**2 for value in bootstrap])),
382
+ "reliability":[{key if key!="positive_rate" else "empirical_relevance":value for key,value in row.items()} for row in bins]}
383
+
384
+
385
+ def report_fitted_calibrator(observations,calibrator,*,bin_count=10,bootstrap_samples=1000,seed=0):
386
+ """Add reproducibility identity to the exact source evaluator result."""
387
+ result=evaluate_fitted_calibrator(observations,calibrator,bin_count=bin_count,bootstrap_samples=bootstrap_samples,seed=seed)
388
+ return {"schema_version":"sphynx.fixed-calibrator-reliability.v1","result":result,
389
+ "observations_digest":value_digest(observations),"calibrator_digest":value_digest(calibrator),
390
+ "bin_count":bin_count,"bootstrap":{"sampling_unit":"passage_observation","rng":"calibrator-fit.mulberry32.v1",
391
+ "seed":seed,"repetitions":bootstrap_samples},"source_digest":SOURCE_PINS["calibrator-fit.ts"],
392
+ "fitting_executed":False,"model_inference_executed":False,"admission_verified":False,
393
+ "notice":"Grader-only source calculation on a fixed calibrator; no fitting, probability admission or cohort independence is established."}
@@ -0,0 +1,204 @@
1
+ """Unreleased local Kido benchmark extensions; not part of the frozen 0.5.0 wheel.
2
+
3
+ Use python -m sphynx.speech_benchmarks. No provider, imported code or model runs.
4
+ """
5
+ from __future__ import annotations
6
+ import argparse
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ import uuid
10
+
11
+ from ._speech_kido import SpeechEvidenceError,digest,encoded,json_value,require
12
+ from . import _speech_benchmark_sources as sources
13
+ from ._speech_benchmark_scoring import (grade_records,evaluate_pt_br_listening,evaluate_pronunciation,
14
+ evaluate_language_switching,evaluate_stt_turn_lifecycle,evaluate_voice_continuity,
15
+ evaluate_code_switching,evaluate_playout_interruptions)
16
+ from .speech_evidence import _new_directory,_read,_write_files,_sha
17
+
18
+ SUITE_ID='speech.kido-benchmarks.v1'
19
+ BUNDLE_SCHEMA='sphynx.speech.kido-benchmarks.bundle.v1'
20
+ REPORT_SCHEMA='sphynx.speech.kido-benchmarks.report.v1'
21
+ INVOCATION_SCHEMA='sphynx.speech.kido-benchmarks.invocation.v1'
22
+ RECEIPT_SCHEMA='sphynx.speech.kido-benchmarks.receipt.v1'
23
+ GRADER='speech.kido-benchmarks.grader.v1'
24
+ MODULES=('speech_benchmarks.py','_speech_benchmark_sources.py','_speech_benchmark_scoring.py','_speech_kido.py','_speech_scoring.py','speech_evidence.py','speech_contracts.py')
25
+ LIMITS=dict(historical_only=True,population_inference=False,new_waveform_quality=False,perceived_latency=False,controlled_barge_in=False)
26
+
27
+
28
+ def list_speech_benchmarks():
29
+ return dict(suite_id=SUITE_ID,invocation_schema=INVOCATION_SCHEMA,grader_version=GRADER,
30
+ protocols=['pt_br_listening','pronunciation','language_switching','stt_turn_lifecycle','voice_continuity','code_switching','playout_interruptions'],
31
+ source_commit=sources.SOURCE_COMMIT,execution='offline retained-evidence grading',
32
+ hosted_admitted=False,provider_calls=False,fresh_quality_ratings=False,
33
+ implementation_sha256={name:digest(Path(__file__).with_name(name).read_bytes()) for name in
34
+ MODULES})
35
+
36
+
37
+ def export_kido_benchmarks_source(repository,output_dir):
38
+ """Export committed benchmark bytes without running Kido's scripts.
39
+
40
+ Git checkout line endings are never treated as canonical source. The
41
+ complete selected export must match the independently reviewed SHA256.
42
+ """
43
+ import hashlib,subprocess
44
+ root=Path(repository).absolute();inventory,_=sources.source_inventory(root)
45
+ paths=list(inventory['files']);batch=''.join(sources.SOURCE_COMMIT+':'+p+'\n' for p in paths).encode()
46
+ try:result=subprocess.run(['git','cat-file','--batch'],input=batch,cwd=root,capture_output=True,check=True,timeout=60)
47
+ except (subprocess.SubprocessError,OSError):raise SpeechEvidenceError('Unable to read the pinned Git benchmark source') from None
48
+ raw=result.stdout;cursor=0;payload={}
49
+ for path in paths:
50
+ end=raw.find(b'\n',cursor);require(end>=cursor,'Incomplete Git object header')
51
+ header=raw[cursor:end].split();require(len(header)==3 and header[1]==b'blob','Missing committed benchmark blob')
52
+ size=int(header[2]);require(0<=size<=16*1024*1024,'Git benchmark blob exceeds limit');cursor=end+1
53
+ data=raw[cursor:cursor+size];cursor+=size
54
+ require(len(data)==size and raw[cursor:cursor+1]==b'\n','Incomplete committed benchmark blob');cursor+=1
55
+ require(hashlib.sha1(b'blob '+str(size).encode()+b'\0'+data).hexdigest().encode()==header[0],'Git benchmark object identity mismatch')
56
+ payload[path]=data
57
+ require(cursor==len(raw) and sum(map(len,payload.values()))<=64*1024*1024,'Git export census or byte limit changed')
58
+ inventory['files']={p:dict(sha256=digest(b),size_bytes=len(b)) for p,b in payload.items()}
59
+ require(digest(encoded(inventory))==sources.SOURCE_LOCK_SHA256,'Git export is not the reviewed benchmark source')
60
+ dest=_new_directory(output_dir);_write_files(dest,payload)
61
+ return dict(path=str(dest),source_commit=sources.SOURCE_COMMIT,source_lock_sha256=sources.SOURCE_LOCK_SHA256,files=len(payload),source_executed=False)
62
+
63
+
64
+ def import_kido_benchmarks(source,output_dir):
65
+ inventory,raw=sources.source_bytes(source);evidence=sources.normalize_sources(raw)
66
+ dest=_new_directory(output_dir)
67
+ files=_write_files(dest,{'source/'+p:b for p,b in raw.items()}|{'evidence.json':encoded(evidence)})
68
+ manifest=dict(schema_version=BUNDLE_SCHEMA,suite_id=SUITE_ID,source_commit=sources.SOURCE_COMMIT,
69
+ source_lock_sha256=digest(encoded(inventory)),files=files,observations=len(evidence['observations']),
70
+ candidate_executed=False,provider_called=False,hosted_admitted=False)
71
+ data=encoded(manifest);(dest/'manifest.json').write_bytes(data)
72
+ return dict(path=str(dest),suite_id=SUITE_ID,manifest_sha256=digest(data),observations=manifest['observations'])
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class SpeechBenchmarks:
77
+ path:Path
78
+ manifest_sha256:str
79
+
80
+
81
+ def _verified(bundle):
82
+ raw=_read(bundle.path,'manifest.json');require(digest(raw)==bundle.manifest_sha256,'Benchmark manifest changed')
83
+ manifest=json_value(raw)
84
+ require(manifest.get('schema_version')==BUNDLE_SCHEMA and manifest.get('suite_id')==SUITE_ID and
85
+ manifest.get('source_commit')==sources.SOURCE_COMMIT and manifest.get('source_lock_sha256')==sources.SOURCE_LOCK_SHA256,
86
+ 'Not the pinned Kido benchmark suite')
87
+ inventory,source=sources.source_bytes(bundle.path/'source')
88
+ expected={'source/'+p for p in source}|{'evidence.json'}
89
+ require(type(manifest.get('files')) is dict and set(manifest['files'])==expected,'Benchmark file census changed')
90
+ for p,receipt in manifest['files'].items():
91
+ data=_read(bundle.path,p);require(receipt==dict(sha256=digest(data),size_bytes=len(data)),'Benchmark artifact changed')
92
+ evidence=sources.normalize_sources(source)
93
+ require(_read(bundle.path,'evidence.json')==encoded(evidence) and manifest.get('observations')==len(evidence['observations']),
94
+ 'Benchmark normalized evidence differs from source')
95
+ require(all(manifest.get(k) is False for k in ('candidate_executed','provider_called','hosted_admitted')),'Benchmark execution boundary changed')
96
+ return manifest,evidence
97
+
98
+
99
+ def load_speech_benchmarks(path,*,expected_manifest_sha256):
100
+ result=SpeechBenchmarks(Path(path).absolute(),_sha(expected_manifest_sha256));_verified(result);return result
101
+
102
+
103
+ def tts_benchmark_cases(bundle,*,benchmark,case_ids=None,language_hint=None):
104
+ """Return TTS inputs only, never provider mappings, ratings, gold or STT data.
105
+
106
+ Mixed-language source requests had no fixed language hint. A caller must
107
+ choose an explicit two-letter hint to adapt them to the current TTS runner.
108
+ """
109
+ import re
110
+ require(benchmark in ('pt-br-listening','pronunciation','code-switching'),'Unknown TTS benchmark')
111
+ _,evidence=_verified(bundle)
112
+ cases=[dict(r) for r in evidence['cases'] if r['id'].startswith(benchmark+'/')]
113
+ from .speech_contracts import identifier
114
+ identity_map={}
115
+ for row in cases:
116
+ source_id=row['id'];row['id']=identifier(source_id.replace('/',':',1))
117
+ require(row['id'] not in identity_map,'Projected TTS case identity collision')
118
+ identity_map[row['id']]=source_id
119
+ if case_ids is not None:
120
+ require(type(case_ids) is list and len(set(case_ids))==len(case_ids),'Invalid selected case IDs')
121
+ require(set(case_ids)<={r['id'] for r in cases},'Selected case absent from benchmark')
122
+ cases=[r for r in cases if r['id'] in case_ids]
123
+ require(cases,'Empty benchmark selection')
124
+ if benchmark=='code-switching':
125
+ require(isinstance(language_hint,str) and re.fullmatch(r'[a-z]{2}',language_hint),'Choose a language hint explicitly for a mixed-language TTS plan')
126
+ for row in cases:row['language']=language_hint
127
+ else:require(language_hint is None,'Do not override the native benchmark language')
128
+ return dict(suite_id=SUITE_ID,benchmark=benchmark,cases=cases,
129
+ case_identity_map={r['id']:identity_map[r['id']] for r in cases},
130
+ cohort_identity=dict(kind=identifier(SUITE_ID+':'+benchmark),sha256=digest(encoded(dict(bundle=bundle.manifest_sha256,cases=cases)))),
131
+ source_bundle_sha256=bundle.manifest_sha256,new_waveform_ratings_available=False)
132
+
133
+
134
+ def grade_speech_benchmarks(bundle,output_dir):
135
+ _,evidence=_verified(bundle);definition=list_speech_benchmarks()
136
+ invocation=dict(schema_version=INVOCATION_SCHEMA,id='speech-benchmark-invocation-'+uuid.uuid4().hex,
137
+ suite_id=SUITE_ID,bundle_manifest_sha256=bundle.manifest_sha256,source_commit=sources.SOURCE_COMMIT,
138
+ source_lock_sha256=sources.SOURCE_LOCK_SHA256,grader_version=GRADER,definition=definition,
139
+ candidate_executed=False,provider_called=False,hosted_admitted=False)
140
+ report=dict(schema_version=REPORT_SCHEMA,suite_id=SUITE_ID,invocation_id=invocation['id'],
141
+ invocation_sha256=digest(encoded(invocation)),results=grade_records(evidence),
142
+ source_audit=evidence['audit'],limits=LIMITS)
143
+ dest=_new_directory(output_dir);files=_write_files(dest,{'invocation.json':encoded(invocation),
144
+ 'report.json':encoded(report),'evidence.json':encoded(evidence)})
145
+ receipt=dict(schema_version=RECEIPT_SCHEMA,suite_id=SUITE_ID,invocation_id=invocation['id'],files=files)
146
+ data=encoded(receipt);(dest/'receipt.json').write_bytes(data)
147
+ return dict(path=str(dest),suite_id=SUITE_ID,invocation_id=invocation['id'],receipt_sha256=digest(data))
148
+
149
+
150
+ def load_speech_benchmark_report(path,*,expected_receipt_sha256):
151
+ root=Path(path).absolute();data=_read(root,'receipt.json')
152
+ require(digest(data)==_sha(expected_receipt_sha256),'Benchmark receipt changed');receipt=json_value(data)
153
+ require(receipt.get('schema_version')==RECEIPT_SCHEMA and receipt.get('suite_id')==SUITE_ID and
154
+ set(receipt.get('files',{}))=={'report.json','invocation.json','evidence.json'},'Not a speech benchmark evaluation')
155
+ payload={}
156
+ for p,h in receipt['files'].items():
157
+ raw=_read(root,p);require(h==dict(sha256=digest(raw),size_bytes=len(raw)),'Benchmark evaluation artifact changed');payload[p]=json_value(raw)
158
+ report,invocation,evidence=(payload[p] for p in ('report.json','invocation.json','evidence.json'))
159
+ require(invocation.get('schema_version')==INVOCATION_SCHEMA and report.get('schema_version')==REPORT_SCHEMA and
160
+ invocation.get('suite_id')==report.get('suite_id')==SUITE_ID and
161
+ invocation.get('id')==report.get('invocation_id')==receipt.get('invocation_id') and
162
+ report.get('invocation_sha256')==digest(encoded(invocation)) and
163
+ invocation.get('source_commit')==sources.SOURCE_COMMIT and invocation.get('source_lock_sha256')==sources.SOURCE_LOCK_SHA256 and
164
+ invocation.get('grader_version')==GRADER and all(invocation.get(k) is False for k in ('candidate_executed','provider_called','hosted_admitted')),
165
+ 'Benchmark report identities or execution boundary changed')
166
+ definition=invocation.get('definition',{})
167
+ require(definition.get('suite_id')==SUITE_ID and definition.get('invocation_schema')==INVOCATION_SCHEMA and
168
+ definition.get('grader_version')==GRADER and definition.get('source_commit')==sources.SOURCE_COMMIT and
169
+ all(definition.get(k) is False for k in ('provider_calls','fresh_quality_ratings','hosted_admitted')),
170
+ 'Benchmark definition boundary changed')
171
+ require(type(definition.get('implementation_sha256')) is dict and set(definition['implementation_sha256'])==set(MODULES),
172
+ 'Benchmark grader source identities missing')
173
+ for value in definition['implementation_sha256'].values():_sha(value)
174
+ require(report.get('limits')==LIMITS and report.get('results')==grade_records(evidence) and report.get('source_audit')==evidence['audit'],'Benchmark calculations or scope disagree with retained evidence')
175
+ return report
176
+
177
+
178
+ def main(argv=None):
179
+ import json
180
+ p=argparse.ArgumentParser(description='Separate offline speech.kido-benchmarks.v1 invocation')
181
+ commands=p.add_subparsers(dest='command',required=True);commands.add_parser('describe')
182
+ export=commands.add_parser('export-source');export.add_argument('--repository',required=True);export.add_argument('--output',required=True)
183
+ i=commands.add_parser('import-kido');i.add_argument('--source',required=True);i.add_argument('--output',required=True)
184
+ for name in ('grade','tts-cases'):
185
+ c=commands.add_parser(name);c.add_argument('--bundle',required=True);c.add_argument('--expected-manifest-sha256',required=True)
186
+ if name=='grade':c.add_argument('--output',required=True)
187
+ else:
188
+ c.add_argument('--benchmark',choices=('pt-br-listening','pronunciation','code-switching'),required=True)
189
+ c.add_argument('--case-id',action='append');c.add_argument('--language-hint')
190
+ args=p.parse_args(argv)
191
+ try:
192
+ if args.command=='describe':result=list_speech_benchmarks()
193
+ elif args.command=='export-source':result=export_kido_benchmarks_source(args.repository,args.output)
194
+ elif args.command=='import-kido':result=import_kido_benchmarks(args.source,args.output)
195
+ else:
196
+ bundle=load_speech_benchmarks(args.bundle,expected_manifest_sha256=args.expected_manifest_sha256)
197
+ result=grade_speech_benchmarks(bundle,args.output) if args.command=='grade' else tts_benchmark_cases(bundle,
198
+ benchmark=args.benchmark,case_ids=args.case_id,language_hint=args.language_hint)
199
+ except (ValueError,OSError,KeyError,TypeError) as error:
200
+ p.exit(2,'Speech benchmark operation failed ('+type(error).__name__+'); check pinned source, IDs and output path.\n')
201
+ print(json.dumps(result,indent=2,ensure_ascii=True))
202
+
203
+
204
+ if __name__=='__main__':main()