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
sphynx/_rag_ranking.py ADDED
@@ -0,0 +1,149 @@
1
+ """Deterministic ranking and evidence-group metric formulas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Mapping, Sequence
7
+
8
+
9
+ def dcg(grades: Sequence[int], depth: int) -> float:
10
+ """Graded discounted cumulative gain using exponential relevance gain."""
11
+
12
+ return float(
13
+ sum(
14
+ (2**grade - 1) / math.log2(rank + 1)
15
+ for rank, grade in enumerate(grades[:depth], start=1)
16
+ )
17
+ )
18
+
19
+
20
+ def ndcg(grades: Sequence[int], depth: int) -> float | None:
21
+ """Normalized DCG, or null when the pool has no relevant item."""
22
+
23
+ ideal = dcg(sorted(grades, reverse=True), depth)
24
+ return None if ideal == 0 else dcg(grades, depth) / ideal
25
+
26
+
27
+ def reciprocal_rank(grades: Sequence[int], threshold: int) -> float:
28
+ for rank, grade in enumerate(grades, start=1):
29
+ if grade >= threshold:
30
+ return 1 / rank
31
+ return 0.0
32
+
33
+
34
+ def hit_rate(grades: Sequence[int], depth: int, threshold: int) -> float:
35
+ return float(any(grade >= threshold for grade in grades[:depth]))
36
+
37
+
38
+ def precision_at(grades: Sequence[int], depth: int, threshold: int) -> float:
39
+ return sum(grade >= threshold for grade in grades[:depth]) / depth
40
+
41
+
42
+ def recall_at(grades: Sequence[int], depth: int, threshold: int) -> float | None:
43
+ relevant = sum(grade >= threshold for grade in grades)
44
+ return None if relevant == 0 else sum(grade >= threshold for grade in grades[:depth]) / relevant
45
+
46
+
47
+ def average_precision(grades: Sequence[int], threshold: int) -> float | None:
48
+ relevant = sum(grade >= threshold for grade in grades)
49
+ if relevant == 0:
50
+ return None
51
+ found = 0
52
+ total = 0.0
53
+ for rank, grade in enumerate(grades, start=1):
54
+ if grade >= threshold:
55
+ found += 1
56
+ total += found / rank
57
+ return total / relevant
58
+
59
+
60
+ def pairwise_accuracy(
61
+ ordered_candidate_ids: Sequence[str], qrels: Mapping[str, int]
62
+ ) -> float | None:
63
+ rank = {candidate_id: index for index, candidate_id in enumerate(ordered_candidate_ids)}
64
+ comparable = 0
65
+ correct = 0
66
+ candidate_ids = tuple(ordered_candidate_ids)
67
+ for left_index, left_id in enumerate(candidate_ids):
68
+ for right_id in candidate_ids[left_index + 1 :]:
69
+ if qrels[left_id] == qrels[right_id]:
70
+ continue
71
+ comparable += 1
72
+ preferred = left_id if qrels[left_id] > qrels[right_id] else right_id
73
+ other = right_id if preferred == left_id else left_id
74
+ correct += int(rank[preferred] < rank[other])
75
+ return None if comparable == 0 else correct / comparable
76
+
77
+
78
+ def weighted_evidence_recall(
79
+ ordered_candidate_ids: Sequence[str],
80
+ candidate_groups: Mapping[str, frozenset[str]],
81
+ group_weights: Mapping[str, float],
82
+ depth: int,
83
+ ) -> float | None:
84
+ total = sum(group_weights.values())
85
+ if total == 0:
86
+ return None
87
+ covered = set().union(
88
+ *(
89
+ candidate_groups.get(candidate_id, frozenset())
90
+ for candidate_id in ordered_candidate_ids[:depth]
91
+ )
92
+ )
93
+ return sum(weight for group, weight in group_weights.items() if group in covered) / total
94
+
95
+
96
+ def alpha_dcg(
97
+ ordered_candidate_ids: Sequence[str],
98
+ candidate_groups: Mapping[str, frozenset[str]],
99
+ group_weights: Mapping[str, float],
100
+ depth: int,
101
+ *,
102
+ alpha: float = 0.5,
103
+ ) -> float:
104
+ seen: dict[str, int] = {}
105
+ total = 0.0
106
+ for rank, candidate_id in enumerate(ordered_candidate_ids[:depth], start=1):
107
+ gain = 0.0
108
+ for group in candidate_groups.get(candidate_id, frozenset()):
109
+ gain += group_weights.get(group, 0.0) * (1 - alpha) ** seen.get(group, 0)
110
+ seen[group] = seen.get(group, 0) + 1
111
+ total += gain / math.log2(rank + 1)
112
+ return total
113
+
114
+
115
+ def alpha_ndcg(
116
+ ordered_candidate_ids: Sequence[str],
117
+ candidate_groups: Mapping[str, frozenset[str]],
118
+ group_weights: Mapping[str, float],
119
+ depth: int,
120
+ *,
121
+ alpha: float = 0.5,
122
+ ) -> float | None:
123
+ """Redundancy-aware nDCG with a deterministic greedy ideal ordering."""
124
+
125
+ remaining = set(ordered_candidate_ids)
126
+ ideal: list[str] = []
127
+ seen: dict[str, int] = {}
128
+ for _ in range(min(depth, len(remaining))):
129
+ best = max(
130
+ remaining,
131
+ key=lambda candidate_id: (
132
+ sum(
133
+ group_weights.get(group, 0.0) * (1 - alpha) ** seen.get(group, 0)
134
+ for group in candidate_groups.get(candidate_id, frozenset())
135
+ ),
136
+ candidate_id,
137
+ ),
138
+ )
139
+ ideal.append(best)
140
+ remaining.remove(best)
141
+ for group in candidate_groups.get(best, frozenset()):
142
+ seen[group] = seen.get(group, 0) + 1
143
+ ideal_score = alpha_dcg(ideal, candidate_groups, group_weights, depth, alpha=alpha)
144
+ if ideal_score == 0:
145
+ return None
146
+ return (
147
+ alpha_dcg(ordered_candidate_ids, candidate_groups, group_weights, depth, alpha=alpha)
148
+ / ideal_score
149
+ )
@@ -0,0 +1,140 @@
1
+ """Descriptive speech benchmark calculations; no model or listener inference."""
2
+ from __future__ import annotations
3
+ from collections import Counter,defaultdict
4
+ import statistics
5
+
6
+ from ._speech_kido import number,require
7
+ from ._speech_scoring import timing_summary
8
+ from ._speech_benchmark_sources import DIMENSIONS
9
+
10
+
11
+ def _groups(rows,extra=()):
12
+ groups=defaultdict(list)
13
+ for row in rows:groups[tuple(row.get(k) for k in ('profile',*extra))].append(row)
14
+ return sorted(groups.items(),key=lambda p:str(p[0]))
15
+
16
+
17
+ def _rating(values,low,high):
18
+ require(all(v is None or type(v) in (int,float) and low<=v<=high for v in values),'Invalid rating')
19
+ present=[v for v in values if v is not None]
20
+ return dict(expected=len(values),rated=len(present),pending=len(values)-len(present),
21
+ mean=statistics.mean(present) if present and len(present)==len(values) else None,
22
+ observed_subset_mean=statistics.mean(present) if present else None,
23
+ distribution={str(v):present.count(v) for v in range(low,high+1)})
24
+
25
+
26
+ def evaluate_pt_br_listening(rows):
27
+ return [dict(profile=key[0],samples=len(group),dimensions={d:_rating([r['ratings'][d] for r in group],1,5) for d in DIMENSIONS},
28
+ historical_rater_count=1,confidence_interval=None,machine_intelligibility=None,
29
+ note='Four rating dimensions are separate; no composite or population inference.') for key,group in _groups(rows)]
30
+
31
+
32
+ def evaluate_pronunciation(rows):
33
+ result=[]
34
+ for key,group in _groups(rows):
35
+ direct=[r for r in group if r['review_scope']=='individually_listened' and r['manifest_bound'] and r['provider_gate'] and not r['customization_observed']]
36
+ assigned=[r for r in group if r['review_scope']=='user_assigned']
37
+ result.append(dict(profile=key[0],lines=len(group),supplied_ratings=_rating([r['score'] for r in group],0,5),
38
+ individually_listened_lines=len(direct),individually_listened_mean=_rating([r['score'] for r in direct],0,5)['mean'] if len(direct)==len(group) else None,
39
+ assigned_lines=len(assigned),assigned_observed_mean=_rating([r['score'] for r in assigned],0,5)['observed_subset_mean'],
40
+ assigned_review_units=1 if assigned else 0,unbound_review_lines=sum(not r['manifest_bound'] for r in group),
41
+ invalid_provider_lines=sum(not r['provider_gate'] for r in group),customized_lines=sum(r['customization_observed'] for r in group),
42
+ historical_rater_count=1,confidence_interval=None,
43
+ categories={category:_rating([r['score'] for r in group if r['category']==category],0,5) for category in sorted({r['category'] for r in group})}))
44
+ return result
45
+
46
+
47
+ def evaluate_language_switching(rows):
48
+ result=[]
49
+ for key,group in _groups(rows,('model','runtime_sha')):
50
+ attempts=[a for r in group for a in r['attempts']]
51
+ valid=[r for r in group if r['valid']]
52
+ values=[a['fresh_ttfa_ms'] for r in valid for a in r['attempts'] if a['fresh_ttfa_ms'] is not None]
53
+ result.append(dict(profile=key[0],model=key[1],runtime_sha=key[2],sessions=len({r['session_id'] for r in group}),
54
+ expected_steps=len(group),valid_steps=len(valid),missing_steps=sum(not r['attempts'] for r in group),
55
+ ambiguous_steps=sum(r.get('alignment')=='ambiguous' for r in group),
56
+ captured_turns=len({(r['session_id'],t) for r in group for t in r['selected_turn_ids']}),
57
+ invalid_or_missing_steps=len(group)-len(valid),attempts=len(attempts),provider_mismatch_attempts=sum(not a['provider_matches'] for a in attempts),
58
+ failed_attempts=sum(not a['success'] for a in attempts),cache_attempts=sum(a['cache_hit'] for a in attempts),
59
+ fresh_valid_ttfa_ms=timing_summary(values,convention='kido.live-median-rounded-index.v1'),
60
+ correct_language_spoken=None,voice_identity_stability=None,perceived_latency=None,confidence_interval=None,
61
+ timing_scope='retained server synthesis start to first bytes; provider validity does not prove spoken language'))
62
+ return result
63
+
64
+
65
+ def evaluate_stt_turn_lifecycle(rows):
66
+ """Check request/turn state transitions without treating snapshots as deltas."""
67
+ results=[]
68
+ for row in rows:
69
+ states={};counts=Counter();errors=[];last=-1;connected=False;request=None;transcripts={}
70
+ require(type(row['events']) is list,'STT events must be a list')
71
+ for event in row['events']:
72
+ require(type(event) is dict,'Invalid STT event')
73
+ now=number(event.get('t'),'STT event clock');kind=event.get('type');counts[kind]+=1
74
+ if not isinstance(event.get('request_id'),str) or not event['request_id']:errors.append('missing_request_identity')
75
+ if now<last:errors.append('reversed_clock')
76
+ last=now
77
+ if kind=='connected':
78
+ if connected:errors.append('duplicate_connection')
79
+ connected=True;request=event.get('request_id');continue
80
+ if not connected:errors.append('event_before_connection')
81
+ if request is not None and event.get('request_id')!=request:errors.append('request_identity_changed')
82
+ turn=event.get('turn_id');state=states.get(turn)
83
+ if not isinstance(turn,str) or not turn:errors.append('missing_turn_identity');continue
84
+ if kind=='turn.start':
85
+ if state is not None or any(v!='ended' for v in states.values()):errors.append('overlapping_turn_start')
86
+ states[turn]='active'
87
+ elif kind=='turn.update':
88
+ if state not in ('active','eager'):errors.append('update_outside_turn')
89
+ elif kind=='turn.eager_end':
90
+ if state!='active':errors.append('eager_end_outside_active_turn')
91
+ states[turn]='eager'
92
+ elif kind=='turn.resume':
93
+ if state!='eager':errors.append('resume_without_eager_end')
94
+ states[turn]='active'
95
+ elif kind=='turn.end':
96
+ if state not in ('active','eager'):errors.append('end_outside_turn')
97
+ states[turn]='ended'
98
+ text=event.get('transcript');require(isinstance(text,str),'Invalid final transcript snapshot')
99
+ transcripts[turn]=text
100
+ else:errors.append('unknown_turn_event')
101
+ if not connected:errors.append('connection_missing')
102
+ if not states or any(state!='ended' for state in states.values()):errors.append('final_turn_missing')
103
+ results.append(dict(id=row['id'],profile=row['profile'],clip_id=row['clip_id'],
104
+ source_status=row['status'],protocol_valid=not errors and not row['error_present'] and row['status'] in ('ok','success'),
105
+ protocol_errors=errors,event_counts=dict(counts),completed_turns=len(transcripts),
106
+ final_snapshots=transcripts,wer=None,endpoint_delay_ms=None,
107
+ note='Retained lifecycle behavior only; audio identity and speech-end ground truth are unavailable.'))
108
+ return results
109
+
110
+
111
+ def evaluate_voice_continuity(rows):
112
+ return [dict(profile=r['profile'],score=r['score'],review_units=1,
113
+ review_scope=r['review_scope'],independent_turn_ratings=0,confidence_interval=None) for r in rows]
114
+
115
+
116
+ def evaluate_code_switching(rows):
117
+ return [dict(profile=key[0],model=key[1],attempts=len(group),successful=sum(r['success'] for r in group),
118
+ failed=sum(not r['success'] for r in group),unique_texts=len({r['case_id'] for r in group}),
119
+ quality_inferred_from_bytes=False,perceived_latency=None,
120
+ observed_success_ttfa_ms=timing_summary([r['ttfa_ms'] for r in group if r['success'] and r['ttfa_ms'] is not None],convention='kido.live-median-rounded-index.v1'),
121
+ timing_scope='retained direct API probe; separate from live-agent switching') for key,group in _groups(rows,('model',))]
122
+
123
+
124
+ def evaluate_playout_interruptions(sessions):
125
+ return [dict(session_id=s['session_id'],profile=s['profile'],
126
+ terminal_playout_deliveries=s['terminal_playout_deliveries'],interrupted_deliveries=s['interrupted_deliveries'],
127
+ controlled_user_barge_in=None,barge_in_success_rate=None,rapid_turn_stress_result=None,
128
+ note='Terminal playout flags are observations, not proof of a controlled microphone interruption.') for s in sessions]
129
+
130
+
131
+ def grade_records(evidence):
132
+ rows=evidence['observations'];by_track={name:[r for r in rows if r['track']==name] for name in {r['track'] for r in rows}}
133
+ evaluators={'pt_br_listening':evaluate_pt_br_listening,'pronunciation':evaluate_pronunciation,
134
+ 'language_switching':evaluate_language_switching,'stt_turn_lifecycle':evaluate_stt_turn_lifecycle,
135
+ 'voice_continuity':evaluate_voice_continuity,'code_switching':evaluate_code_switching}
136
+ require(set(by_track)==set(evaluators),'Benchmark track census changed')
137
+ result={name:fn(by_track[name]) for name,fn in evaluators.items()}
138
+ sessions=evidence['audit']['pronunciation']['sessions']+evidence['audit']['switching_sessions']
139
+ result['playout_interruptions']=evaluate_playout_interruptions(sessions)
140
+ return result
@@ -0,0 +1,275 @@
1
+ """Native Kido benchmark intake. Source files are inert, private evidence."""
2
+ from __future__ import annotations
3
+
4
+ from collections import Counter
5
+ from pathlib import Path
6
+ import re
7
+
8
+ from ._speech_kido import csv_rows, digest, encoded, json_value, number, require
9
+ from .speech_evidence import _plain, _read
10
+
11
+ SOURCE_COMMIT = '71ec7e89805360479ea19c5bb43fae7707229ce9'
12
+ SOURCE_REPOSITORY = 'https://github.com/sable-inc/sable-tts-eval'
13
+ # Reviewed against committed Git blobs; no session inventory ships in the wheel.
14
+ SOURCE_LOCK_SHA256 = 'e043aa19f68be82974120c894b2b0276bf7ba538db2002084d65a33ecf14bfab'
15
+ PT = 'results/isolated/multilingual-extension-v1/pt-br-listening-pack/'
16
+ PRON = 'results/live-agent/pronunciation-v1/'
17
+ SWITCH = 'results/live-agent/language-switching-v1/'
18
+ TURN = 'results/exploratory/cartesia-ink-2-stt/'
19
+ DIMENSIONS = ('overall_quality_1_5', 'pronunciation_1_5',
20
+ 'native_sounding_1_5', 'voice_consistency_1_5')
21
+
22
+
23
+ def source_inventory(root):
24
+ root = Path(root).absolute(); _plain(root)
25
+ paths = {'harness/corpus/pt-br-listening-pack-v1.json',
26
+ PT+'mapping.json', PT+'scores-completed.csv', PT+'manifest.json',
27
+ PRON+'listening-review/manifest.csv', PRON+'listening-review/human-review-scored.csv',
28
+ SWITCH+'track-b-voice-continuity/README.md',
29
+ SWITCH+'track-c-code-switching/README.md',
30
+ SWITCH+'track-c-code-switching/term_list.json',
31
+ SWITCH+'track-c-code-switching/code_switch_results.json'}
32
+ for folder in ('turn-detection-v1', 'turn-detection-ink2-v1'):
33
+ paths.update(TURN+folder+'/'+p for p in ('turn_events_raw.json', 'pause_clips_manifest.json', 'run_turn_test.py'))
34
+ for prefix in (PRON, SWITCH):
35
+ base = root/prefix/'sessions'; _plain(base)
36
+ for directory in sorted(base.glob('*/RM_*')):
37
+ _plain(directory)
38
+ if not directory.is_dir(): continue
39
+ paths.update(p.relative_to(root).as_posix() for p in directory.glob('events/chunk_*.jsonl'))
40
+ paths.update((directory/p).relative_to(root).as_posix() for p in ('session.json', 'metadata.json', 'agent_config.json'))
41
+ if prefix == SWITCH:
42
+ paths.add((directory.parent/'summary.md').relative_to(root).as_posix())
43
+ paths.update(p.relative_to(root).as_posix() for p in directory.glob('record/transcript/chunk_*.jsonl'))
44
+ require(0 < len(paths) <= 1000, 'Invalid benchmark source census')
45
+ data = {}; total = 0
46
+ for p in sorted(paths):
47
+ raw = _read(root, p); total += len(raw)
48
+ require(total <= 64*1024*1024, 'Benchmark source exceeds byte limit')
49
+ data[p] = raw
50
+ inventory = dict(schema_version='sphynx.speech.kido-source.v1', repository=SOURCE_REPOSITORY,
51
+ source_commit=SOURCE_COMMIT, files={p:dict(sha256=digest(b),size_bytes=len(b)) for p,b in data.items()})
52
+ return inventory, data
53
+
54
+
55
+ def source_bytes(root):
56
+ inventory, data = source_inventory(root)
57
+ require(digest(encoded(inventory)) == SOURCE_LOCK_SHA256, 'Benchmark source differs from reviewed bytes')
58
+ return inventory, data
59
+
60
+
61
+ def _unique(rows, key, label):
62
+ identities = [key(r) for r in rows]
63
+ require(len(set(identities)) == len(identities), 'Duplicate '+label)
64
+ return dict(zip(identities, rows))
65
+
66
+
67
+ def _score(value, low, high):
68
+ if value is None or str(value).strip() == '': return None
69
+ require(isinstance(value, str) and re.fullmatch(r'[0-9]+', value), 'Rating must be an integer')
70
+ result = int(value); require(low <= result <= high, 'Rating out of range')
71
+ return result
72
+
73
+
74
+ def _case(cases, benchmark, case_id, text, language):
75
+ require(isinstance(text, str) and 0 < len(text) <= 10000, 'Invalid benchmark input text')
76
+ identity = benchmark+'/'+case_id
77
+ value = dict(id=identity, text=text, language=language)
78
+ require(identity not in cases or cases[identity] == value, 'Conflicting benchmark input identity')
79
+ cases[identity] = value
80
+ return identity
81
+
82
+
83
+ def _profile(provider, model):
84
+ return {'eleven_v4':'eleven-v4', 'eleven_v4_turbo':'eleven-v4-turbo'}.get(model, provider)
85
+
86
+
87
+ def synthesis_record(event, provider):
88
+ d = event['data']
89
+ require(type(d) is dict and all(type(d.get(k)) is bool for k in ('complete','cancelled','cache_hit')), 'Invalid synthesis flags')
90
+ audio = d.get('produced_audio_bytes'); require(type(audio) is int and audio >= 0, 'Invalid synthesis audio count')
91
+ error = d.get('error') or ''; require(isinstance(error,str), 'Invalid synthesis error')
92
+ observed = d.get('provider') or None
93
+ success = d['complete'] and not d['cancelled'] and not error and audio > 0
94
+ clocks = [number(d.get(k), k, nullable=True) for k in ('started_at','first_audio_at','ended_at')]
95
+ ordered = all(v is not None for v in clocks) and clocks[0] <= clocks[1] <= clocks[2]
96
+ return dict(turn_id=event.get('turn_id'), response_id=d.get('response_id'),
97
+ success=success, provider_matches=observed == provider, observed_provider=observed,
98
+ observed_model=d.get('model') or None, cache_hit=d['cache_hit'], cancelled=d['cancelled'],
99
+ error_present=bool(error), audio_bytes=audio,
100
+ fresh_ttfa_ms=(clocks[1]-clocks[0])*1000 if success and observed == provider and not d['cache_hit'] and ordered else None)
101
+
102
+
103
+ def session_records(files, prefix):
104
+ result = []
105
+ for path in sorted(p for p in files if p.startswith(prefix+'sessions/') and p.endswith('/session.json')):
106
+ base = path.removesuffix('session.json'); metadata = json_value(files[base+'metadata.json'])
107
+ config = json_value(files[base+'agent_config.json']); tts = config.get('tts', {})
108
+ provider, model = tts.get('provider'), tts.get('model')
109
+ require(isinstance(provider,str) and isinstance(model,str), 'Missing configured speech identity')
110
+ chunks = sorted(p for p in files if p.startswith(base+'events/chunk_') and p.endswith('.jsonl'))
111
+ nums = [int(re.search(r'chunk_([0-9]+)\.jsonl$', p)[1]) for p in chunks]
112
+ require(nums and nums == list(range(nums[0], nums[0]+len(nums))), 'Missing or unordered benchmark event chunks')
113
+ events = [json_value(line) for p in chunks for line in files[p].splitlines() if line.strip()]
114
+ _unique(events, lambda e:e['id'], 'session event ID')
115
+ synthesis = [synthesis_record(e, provider) for e in events if e.get('type') == 'synthesis_complete']
116
+ valid = bool(synthesis) and all(s['success'] and s['provider_matches'] for s in synthesis)
117
+ # Last transport playout span per delivery is the terminal observation.
118
+ playout = {}
119
+ for e in events:
120
+ d = e.get('data', {})
121
+ if e.get('type') == 'turn_trace' and d.get('name') == 'playout' and d.get('measurement') == 'transport_buffer_estimate':
122
+ require(isinstance(d.get('delivery_id'),str) and type(d.get('interrupted')) is bool, 'Invalid playout event')
123
+ playout[d['delivery_id']] = dict(interrupted=d['interrupted'], complete=d.get('complete'))
124
+ result.append(dict(session_id=base.rstrip('/').split('/')[-1], base=base, provider=provider, model=model,
125
+ profile=_profile(provider,model), runtime_sha=metadata.get('git_sha'), provider_gate=valid,
126
+ attempts=synthesis, interrupted_deliveries=sum(x['interrupted'] for x in playout.values()),
127
+ terminal_playout_deliveries=len(playout), record=json_value(files[path]).get('record', {}),
128
+ config_sha256=digest(files[base+'agent_config.json']),
129
+ transcripts=[json_value(line) for p in sorted(files) if p.startswith(base+'record/transcript/chunk_') for line in files[p].splitlines() if line.strip()],
130
+ pronunciation_customization=bool(tts.get('pronunciation_dictionary_id') or tts.get('pronunciation_rules') or tts.get('pronunciation_terms'))))
131
+ return result
132
+
133
+
134
+ def _markdown_scores(raw, heading):
135
+ text = raw.decode('utf-8'); lines = text.splitlines(); result = []
136
+ for index, line in enumerate(lines):
137
+ if not line.startswith('|') or heading not in line: continue
138
+ for row in lines[index+2:]:
139
+ if not row.startswith('|'): break
140
+ columns = [v.strip() for v in row.strip('|').split('|')]
141
+ require(len(columns)==2 and re.fullmatch(r'[1-5](?:\.0)?',columns[1]) is not None, 'Invalid qualitative score table')
142
+ result.append((columns[0],float(columns[1])))
143
+ require(result, 'Missing qualitative review table')
144
+ return result
145
+
146
+
147
+ def exact_script_turns(transcripts,text):
148
+ """Match whole scripts or adjacent agent fragments without fuzzy matching.
149
+
150
+ User turns, repeated snapshots, unrelated text and ambiguous matches do
151
+ not become an alignment. Each matched synthesis delivery stays separate.
152
+ """
153
+ norm=lambda value:' '.join(value.split())
154
+ target=norm(text);matches=set()
155
+ direct={r['turn_id'] for r in transcripts if r.get('speaker')=='agent' and r.get('turn_id') and norm(r.get('text',''))==target}
156
+ if direct:return (sorted(direct),'exact-single-delivery' if len(direct)==1 else 'ambiguous')
157
+ for start in range(len(transcripts)):
158
+ segments=[];turns=[]
159
+ for row in transcripts[start:start+4]:
160
+ if row.get('speaker')!='agent' or not row.get('turn_id') or row['turn_id'] in turns:break
161
+ segments.append(row.get('text',''));turns.append(row['turn_id']);combined=norm(' '.join(segments))
162
+ if combined==target:matches.add(tuple(turns));break
163
+ if not target.startswith(combined):break
164
+ if len(matches)==1:return (list(next(iter(matches))),'exact-adjacent-agent-fragments')
165
+ return ([], 'ambiguous' if matches else 'unavailable')
166
+
167
+
168
+ def normalize_sources(files):
169
+ observations=[]; cases={}; audit={}
170
+ corpus=json_value(files['harness/corpus/pt-br-listening-pack-v1.json'])
171
+ lines=_unique(corpus['lines'],lambda r:r['id'],'PT-BR corpus line')
172
+ require(corpus['line_count']==len(lines) and corpus['language']=='pt-BR','PT-BR corpus identity mismatch')
173
+ mapping=json_value(files[PT+'mapping.json'])['samples']
174
+ scores=_unique(csv_rows(files[PT+'scores-completed.csv']),lambda r:r['sample_id'],'PT-BR rating')
175
+ require(set(scores)<=set(mapping),'PT-BR rating has no mapped sample')
176
+ manifest=json_value(files[PT+'manifest.json'])
177
+ identities=_unique(manifest['providers_synthesized'],lambda r:_profile(r['provider'],r['model']),'PT-BR model profile')
178
+ for sample, row in sorted(mapping.items()):
179
+ require(row['line_id'] in lines and row['provider'] in identities,'Unbound PT-BR sample')
180
+ line=lines[row['line_id']]; require(row['text']==line['text'] and row['category']==line['category'],'PT-BR mapping disagrees with corpus')
181
+ case_id=_case(cases,'pt-br-listening',row['line_id'],line['text'],'pt-BR')
182
+ observations.append(dict(track='pt_br_listening',id='pt-br/'+sample,profile=row['provider'],
183
+ model=identities[row['provider']]['model'],case_id=case_id,category=line['category'],
184
+ ratings={dim:_score(scores.get(sample,{}).get(dim),1,5) for dim in DIMENSIONS},
185
+ review_scope='historical-single-rater',audio_available=False))
186
+ audit['pt_br']=dict(mapped_samples=len(mapping),completed_rows=len(scores),
187
+ pending_samples=len(mapping)-len(scores),machine_intelligibility=None,
188
+ synthesis_manifest=manifest,latency_status='supplied-summary-only-not-recomputed-from-trials')
189
+
190
+ pron_sessions=session_records(files,PRON); switch_sessions=session_records(files,SWITCH)
191
+ manifest=_unique(csv_rows(files[PRON+'listening-review/manifest.csv']),lambda r:(r['provider'],r['line']),'pronunciation manifest row')
192
+ ratings=_unique(csv_rows(files[PRON+'listening-review/human-review-scored.csv']),lambda r:(r['provider'],r['line_id']),'pronunciation rating')
193
+ profiles={}
194
+ for session in pron_sessions: profiles.setdefault(session['profile'],[]).append(session)
195
+ for (profile,line_id),row in sorted(ratings.items()):
196
+ require(profile in profiles,'Pronunciation review has no captured profile')
197
+ m=manifest.get((profile,line_id)); scope=row['review_source']; score=_score(row['human_score_0_5'],0,5)
198
+ require(scope in ('individually_listened','user_assigned','pending') and (score is None)==(scope=='pending'),'Pronunciation review status mismatch')
199
+ if m:
200
+ require(m['category']==row['category'] and m['expected_entity']==row['expected_entity'],'Pronunciation review mapping mismatch')
201
+ case_id=_case(cases,'pronunciation','line-'+line_id+'-'+digest(m['corpus_text'].encode())[:12],m['corpus_text'],'en')
202
+ else: case_id=None
203
+ sessions=profiles[profile]
204
+ observations.append(dict(track='pronunciation',id='pronunciation/'+profile+'/'+line_id,profile=profile,
205
+ model=sessions[0]['model'],category=row['category'],case_id=case_id,score=score,review_scope=scope,
206
+ manifest_bound=m is not None,provider_gate=all(s['provider_gate'] for s in sessions),
207
+ customization_observed=any(s['pronunciation_customization'] for s in sessions),audio_available=False))
208
+ audit['pronunciation']=dict(mapped_rows=len(manifest),rating_rows=len(ratings),
209
+ unbound_review_rows=sum(k not in manifest for k in ratings),
210
+ sessions=[{k:v for k,v in s.items() if k not in ('base','transcripts')} for s in pron_sessions])
211
+
212
+ for s in switch_sessions:
213
+ summary=files[str(Path(s['base']).parent/'summary.md').replace('\\','/')]
214
+ selected=[]
215
+ for line in summary.decode('utf-8').splitlines():
216
+ cols=[x.strip() for x in line.strip('|').split('|')]
217
+ if line.startswith('|') and re.fullmatch(r'turn_[0-9]+',cols[0]) and not cols[1].startswith('*(unscripted'):
218
+ selected.append(([cols[0]],cols[1]))
219
+ basis='native-session-summary-turn-IDs'
220
+ if s['profile']=='google' and not selected:
221
+ # Google summaries lack turn IDs. Join exact scripted text to retained
222
+ # transcript records, never guess the last eight synthesis events.
223
+ basis='exact-script-to-retained-transcript-join';in_google=False
224
+ for line in files[SWITCH+'track-b-voice-continuity/README.md'].decode('utf-8').splitlines():
225
+ if line.startswith('## '):in_google=line.startswith('## Google')
226
+ cols=[x.strip() for x in line.strip('|').split('|')]
227
+ if in_google and line.startswith('|') and re.fullmatch(r'[0-9]{2}:[0-9]{2}',cols[0]) and cols[1]!='English (first)':
228
+ text=cols[2].strip('"“”')
229
+ turns,alignment=exact_script_turns(s['transcripts'],text)
230
+ selected.append((turns,cols[1],alignment))
231
+ selected=[(*row,'native-turn-ID') if len(row)==2 else row for row in selected]
232
+ require(len(selected)==8 and len({r[1] for r in selected})==8,'Switching selection is not the declared eight-step protocol')
233
+ require(len({t for turns,_,_ in selected for t in turns})==sum(len(turns) for turns,_,_ in selected),'Switching turn mapped to multiple steps')
234
+ for index,(turns,language,alignment) in enumerate(selected):
235
+ attempts=[a for a in s['attempts'] if a['turn_id'] in turns]
236
+ observations.append(dict(track='language_switching',id='switch/'+s['session_id']+'/step-'+str(index+1),
237
+ profile=s['profile'],model=s['model'],session_id=s['session_id'],runtime_sha=s['runtime_sha'],
238
+ language=language,selected_turn_ids=turns,selection_basis=basis,alignment=alignment,attempts=attempts,
239
+ valid=alignment!='ambiguous' and bool(attempts) and all(a['success'] and a['provider_matches'] for a in attempts)))
240
+ audit['switching_sessions']=[{k:v for k,v in s.items() if k not in ('base','transcripts')} for s in switch_sessions]
241
+
242
+ for folder,model in [('turn-detection-v1','ink-preview'),('turn-detection-ink2-v1','ink-2')]:
243
+ prefix=TURN+folder+'/'
244
+ # AST parsing reads constants only, never imports the original provider script.
245
+ import ast
246
+ tree=ast.parse(files[prefix+'run_turn_test.py'].decode('utf-8'))
247
+ constants={n.targets[0].id:n.value.value for n in tree.body if isinstance(n,ast.Assign) and len(n.targets)==1 and isinstance(n.targets[0],ast.Name) and isinstance(n.value,ast.Constant)}
248
+ require(constants.get('MODEL')==model and str(constants.get('WS_URL_BASE','')).endswith('/stt/turns/websocket'),'STT endpoint or requested model mismatch')
249
+ clips=_unique(json_value(files[prefix+'pause_clips_manifest.json']),lambda r:r['name'],'pause clip')
250
+ trials=_unique(json_value(files[prefix+'turn_events_raw.json']),lambda r:r['name'],'turn lifecycle attempt')
251
+ require(set(clips)==set(trials),'Missing STT lifecycle attempts')
252
+ for name,row in trials.items():
253
+ observations.append(dict(track='stt_turn_lifecycle',id='turn/'+model+'/'+name,profile=model,
254
+ clip_id=name,status=row['status'],error_present=bool(row.get('error')),events=row['events'],
255
+ audio_duration_s=number(clips[name]['duration_s'],'clip duration'),audio_available=False))
256
+
257
+ review_aliases={'ElevenLabs':'elevenlabs','Cartesia':'cartesia','MiniMax':'minimax','Google':'google'}
258
+ for provider,score in _markdown_scores(files[SWITCH+'track-b-voice-continuity/README.md'],'Voice continuity'):
259
+ require(provider in review_aliases,'Unknown continuity review profile')
260
+ profile=review_aliases[provider]; require(any(s['profile']==profile for s in switch_sessions),'Unbound continuity review')
261
+ observations.append(dict(track='voice_continuity',id='continuity/'+profile,profile=profile,score=score,
262
+ review_scope='historical-provider-level-qualitative-single-rater',independent_turn_ratings=0,audio_available=False))
263
+ quality=_markdown_scores(files[SWITCH+'track-c-code-switching/README.md'],'Qualitative score')
264
+ audit['code_switching_quality']=[dict(language_pair=pair,score=score,review_scope='historical-language-pair-level-single-rater') for pair,score in quality]
265
+ trials=json_value(files[SWITCH+'track-c-code-switching/code_switch_results.json'])
266
+ _unique(trials,lambda r:(r['id'],r['rep']),'code-switching attempt')
267
+ for row in trials:
268
+ case_id=_case(cases,'code-switching',row['id'],row['text'],'mul')
269
+ observations.append(dict(track='code_switching',id='code-switch/'+row['id']+'/'+str(row['rep']),
270
+ profile=row['provider'],model=row['model'],case_id=case_id,
271
+ success=row['status']=='success' and not row.get('error') and number(row['audio_bytes'],'code-switch audio')>0,
272
+ ttfa_ms=number(row['ttfa_ms'],'code-switch TTFA',nullable=True),audio_available=False))
273
+ require(len({r['id'] for r in observations})==len(observations),'Duplicate benchmark observation')
274
+ audit['source_scope']='selected retained benchmark files; no audio, source execution, new listening or provider calls'
275
+ return dict(observations=observations,cases=sorted(cases.values(),key=lambda r:r['id']),audit=audit)