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
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)
|