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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- 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()
|