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,378 @@
|
|
|
1
|
+
"""Frozen speech protocol plans, explicit capture ingestion and offline fixtures.
|
|
2
|
+
|
|
3
|
+
No account access, microphone, provider or live session is launched here. Use
|
|
4
|
+
the separate TTS runner for TTS execution, or a reviewed collector for live
|
|
5
|
+
campaigns. Missing captures remain in every denominator.
|
|
6
|
+
"""
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import argparse
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import uuid
|
|
14
|
+
import wave
|
|
15
|
+
|
|
16
|
+
from ._speech_kido import digest,encoded,json_value,require,number
|
|
17
|
+
from ._speech_scoring import score_transcript
|
|
18
|
+
from .speech_contracts import identifier
|
|
19
|
+
from .speech_evidence import _new_directory,_read,_write_files,_sha
|
|
20
|
+
|
|
21
|
+
SUITE_ID='speech.kido-campaigns.v1'
|
|
22
|
+
SCHEMA='sphynx.speech.kido-campaigns'
|
|
23
|
+
PROTOCOLS=('session-reliability','controlled-barge-in','rapid-turn-stress','machine-intelligibility')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _candidate(value):
|
|
27
|
+
require(type(value) is dict and set(value)=={'id','provider','model','voice_id','source_commit','source_pr'},'Candidate identity fields differ')
|
|
28
|
+
for k in ('id','provider','model'):identifier(value[k])
|
|
29
|
+
require(isinstance(value['source_commit'],str) and len(value['source_commit'])==40
|
|
30
|
+
and all(c in '0123456789abcdef' for c in value['source_commit']),'Pin full candidate commit')
|
|
31
|
+
import re
|
|
32
|
+
require(isinstance(value['source_pr'],str) and re.fullmatch(r'https://github.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/pull/[1-9][0-9]*',value['source_pr']),'Pin candidate PR')
|
|
33
|
+
require(value['voice_id'] is None or isinstance(value['voice_id'],str) and 0<len(value['voice_id'])<=128,'Invalid voice identity')
|
|
34
|
+
return dict(value)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _audio(path):
|
|
38
|
+
path=Path(path).absolute();raw=_read(path.parent,path.name)
|
|
39
|
+
try:
|
|
40
|
+
with wave.open(io.BytesIO(raw),'rb') as reader:
|
|
41
|
+
require(reader.getnchannels()==1 and reader.getsampwidth()==2 and reader.getcomptype()=='NONE'
|
|
42
|
+
and reader.getframerate() in (8000,16000,24000,44100,48000),'Use bounded mono PCM16 WAV audio')
|
|
43
|
+
count=reader.getnframes();rate=reader.getframerate();pcm=reader.readframes(count)
|
|
44
|
+
require(count>0 and count<=rate*120 and len(pcm)==count*2,'Empty, truncated or oversized PCM audio')
|
|
45
|
+
except (wave.Error,EOFError):raise ValueError('Invalid campaign WAV') from None
|
|
46
|
+
return raw,dict(sha256=digest(raw),size_bytes=len(raw),sample_rate=rate,frames=count,duration_ms=count/rate*1000)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def freeze_speech_campaign(bundle,*,protocol,candidates,output_dir,mode='capture',sessions_per_candidate=20,
|
|
50
|
+
rapid_turns=8,max_idle_ms=200,interrupt_after_ms=500,interrupt_tolerance_ms=250,
|
|
51
|
+
microphone_audio=None,audio_cases=None,asr=None,max_units=200,collector_source_sha256=None):
|
|
52
|
+
"""Freeze an exact design. Audio references stay out of collector inputs."""
|
|
53
|
+
from .speech_complete import _verified
|
|
54
|
+
evidence=_verified(bundle)
|
|
55
|
+
require(protocol in PROTOCOLS and mode in ('capture','fixture'),'Unknown campaign protocol or mode')
|
|
56
|
+
if collector_source_sha256 is not None:_sha(collector_source_sha256)
|
|
57
|
+
require(type(candidates) is list and 1<=len(candidates)<=16,'Invalid candidate census')
|
|
58
|
+
candidates=[_candidate(c) for c in candidates]
|
|
59
|
+
require(len({c['id'] for c in candidates})==len(candidates),'Duplicate candidate')
|
|
60
|
+
require(type(sessions_per_candidate) is int and 1<=sessions_per_candidate<=100,'Invalid sessions per candidate')
|
|
61
|
+
require(type(max_units) is int and 1<=max_units<=200,'Invalid campaign unit cap')
|
|
62
|
+
for v in (max_idle_ms,interrupt_after_ms,interrupt_tolerance_ms):number(v,'campaign timing')
|
|
63
|
+
require(0<interrupt_tolerance_ms<=1000 and 0<interrupt_after_ms<=10000 and max_idle_ms<=2000,'Campaign timing exceeds limits')
|
|
64
|
+
require(type(rapid_turns) is int and 8<=rapid_turns<=10,'Rapid-turn design requires eight to ten turns')
|
|
65
|
+
payload={};references={};script=evidence['official_script']
|
|
66
|
+
if protocol=='session-reliability':steps=script[:4]
|
|
67
|
+
elif protocol=='controlled-barge-in':steps=script[5:7]
|
|
68
|
+
elif protocol=='rapid-turn-stress':steps=[script[6+i%4] for i in range(rapid_turns)]
|
|
69
|
+
else:steps=[]
|
|
70
|
+
steps=[dict(id='step:'+str(i+1),case_id=s['id'],text=s['text'],language=s['language']) for i,s in enumerate(steps)]
|
|
71
|
+
microphone=None
|
|
72
|
+
if protocol=='controlled-barge-in':
|
|
73
|
+
if mode=='capture':
|
|
74
|
+
require(microphone_audio is not None,'Freeze actual interruption audio before a microphone campaign')
|
|
75
|
+
raw,microphone=_audio(microphone_audio);payload['audio/microphone.wav']=raw
|
|
76
|
+
else:microphone=dict(sha256=digest(b'fixture microphone'),duration_ms=100,fixture=True)
|
|
77
|
+
if protocol=='machine-intelligibility':
|
|
78
|
+
require(type(asr) is dict and set(asr)=={'provider','model','revision','config'} and all(isinstance(asr[k],str) and asr[k] for k in ('provider','model','revision'))
|
|
79
|
+
and asr['revision'].lower() not in ('latest','default','auto') and type(asr['config']) is dict,'Freeze ASR revision and configuration')
|
|
80
|
+
require(type(audio_cases) is list and 0<len(audio_cases)<=100,'Supply approved audio cases')
|
|
81
|
+
inputs=[]
|
|
82
|
+
for row in audio_cases:
|
|
83
|
+
require(type(row) is dict and set(row)=={'id','path','reference','language','candidate_id'},'Audio case fields differ')
|
|
84
|
+
identifier(row['id']);require(isinstance(row['reference'],str) and len(row['reference'])<=10000,'Invalid reference')
|
|
85
|
+
import re
|
|
86
|
+
require(isinstance(row['language'],str) and re.fullmatch('[a-z]{2}(?:-[A-Za-z]{2,4})?',row['language']),'Invalid audio language')
|
|
87
|
+
require(row['candidate_id'] in {c['id'] for c in candidates},'Unknown audio candidate')
|
|
88
|
+
raw,meta=_audio(row['path']);name='audio/'+row['id'].replace(':','_')+'.wav'
|
|
89
|
+
require(name not in payload,'Duplicate audio identity');payload[name]=raw
|
|
90
|
+
inputs.append(dict(id=row['id'],candidate_id=row['candidate_id'],language=row['language'],audio=meta,audio_file=name))
|
|
91
|
+
references[row['id']]=row['reference']
|
|
92
|
+
require(len({r['id'] for r in inputs})==len(inputs),'Duplicate audio case ID')
|
|
93
|
+
units=[dict(id='unit:'+str(i+1),candidate_id=r['candidate_id'],audio_case=r,steps=[]) for i,r in enumerate(inputs)]
|
|
94
|
+
else:
|
|
95
|
+
require(asr is None and audio_cases is None,'ASR inputs belong only to machine intelligibility')
|
|
96
|
+
units=[dict(id='unit:'+str(i+1),candidate_id=c['id'],steps=steps) for i,(c,_) in enumerate(
|
|
97
|
+
(c,n) for c in candidates for n in range(sessions_per_candidate))]
|
|
98
|
+
require(len(units)<=max_units,'Campaign exceeds explicit unit cap')
|
|
99
|
+
plan=dict(schema_version=SCHEMA+'.plan.v1',suite_id=SUITE_ID,id='speech-campaign-'+uuid.uuid4().hex,
|
|
100
|
+
protocol=protocol,mode=mode,bundle_manifest_sha256=bundle.manifest_sha256,candidates=candidates,units=units,
|
|
101
|
+
source_commit='71ec7e89805360479ea19c5bb43fae7707229ce9',microphone=microphone,asr=asr,
|
|
102
|
+
design=dict(sessions_per_candidate=sessions_per_candidate,rapid_turns=rapid_turns,max_idle_ms=max_idle_ms,
|
|
103
|
+
interrupt_after_ms=interrupt_after_ms,interrupt_tolerance_ms=interrupt_tolerance_ms,max_units=max_units),
|
|
104
|
+
planned_units=len(units),planned_steps=sum(len(u['steps']) for u in units),provider_calls_made=False,
|
|
105
|
+
provider_cost_usd=None,monetary_cap_enforced=False,collector_source_verified=False,collector_source_sha256=collector_source_sha256,
|
|
106
|
+
implementation_sha256={p:digest(Path(__file__).with_name(p).read_bytes()) for p in ('speech_campaigns.py','_speech_scoring.py','speech_contracts.py')})
|
|
107
|
+
files=_write_files(_new_directory(output_dir),payload|{'references.json':encoded(references)})
|
|
108
|
+
dest=Path(output_dir).absolute();plan['files']=files;raw=encoded(plan);(dest/'plan.json').write_bytes(raw)
|
|
109
|
+
return dict(path=str(dest),plan_sha256=digest(raw),suite_id=SUITE_ID,protocol=protocol,planned_units=len(units),planned_steps=plan['planned_steps'])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _load_plan(path,pin):
|
|
113
|
+
root=Path(path).absolute();raw=_read(root,'plan.json');require(digest(raw)==_sha(pin),'Campaign plan changed')
|
|
114
|
+
plan=json_value(raw)
|
|
115
|
+
require(plan.get('schema_version')==SCHEMA+'.plan.v1' and plan.get('suite_id')==SUITE_ID and
|
|
116
|
+
plan.get('protocol') in PROTOCOLS and plan.get('mode') in ('fixture','capture') and plan.get('provider_calls_made') is False,'Invalid campaign plan')
|
|
117
|
+
require(type(plan.get('units')) is list and len(plan['units'])==plan['planned_units'] and
|
|
118
|
+
len({u['id'] for u in plan['units']})==len(plan['units']),'Campaign unit census changed')
|
|
119
|
+
for c in plan['candidates']:_candidate(c)
|
|
120
|
+
for p,h in plan.get('files',{}).items():
|
|
121
|
+
b=_read(root,p);require(h==dict(sha256=digest(b),size_bytes=len(b)),'Campaign input artifact changed')
|
|
122
|
+
require('references.json' in plan['files'],'Campaign references missing')
|
|
123
|
+
return root,plan,json_value(_read(root,'references.json'))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def speech_campaign_requests(path,*,expected_plan_sha256):
|
|
127
|
+
"""Collector view excludes reference text and unrelated repository data."""
|
|
128
|
+
root,plan,_=_load_plan(path,expected_plan_sha256)
|
|
129
|
+
return dict(plan_sha256=expected_plan_sha256,protocol=plan['protocol'],mode=plan['mode'],candidates=plan['candidates'],
|
|
130
|
+
design=plan['design'],asr=plan['asr'],microphone=plan['microphone'],units=plan['units'],
|
|
131
|
+
input_files={p:h for p,h in plan['files'].items() if p!='references.json'},
|
|
132
|
+
collector='reviewed external collector required for real sessions; this call launches nothing')
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _captures(plan,rows):
|
|
136
|
+
require(plan.get('schema_version')==SCHEMA+'.plan.v1' and plan.get('suite_id')==SUITE_ID and
|
|
137
|
+
plan.get('protocol') in PROTOCOLS and plan.get('mode') in ('fixture','capture') and
|
|
138
|
+
type(plan.get('units')) is list and 0<len(plan['units'])==plan.get('planned_units')<=200 and
|
|
139
|
+
len({u['id'] for u in plan['units']})==len(plan['units']),'Invalid frozen campaign census or scope')
|
|
140
|
+
require(type(plan.get('candidates')) is list and 0<len(plan['candidates'])<=16 and
|
|
141
|
+
len({c['id'] for c in plan['candidates']})==len(plan['candidates']),'Invalid frozen candidate census')
|
|
142
|
+
for c in plan['candidates']:_candidate(c)
|
|
143
|
+
require(type(rows) is list and len(rows)<=plan['planned_units'],'Invalid campaign capture census')
|
|
144
|
+
expected={u['id']:u for u in plan['units']};seen=set();sessions=set();result={}
|
|
145
|
+
candidates={c['id']:c for c in plan['candidates']}
|
|
146
|
+
for row in rows:
|
|
147
|
+
require(type(row) is dict and row.get('unit_id') in expected and row['unit_id'] not in seen,'Unknown or duplicate captured unit')
|
|
148
|
+
unit=expected[row['unit_id']];candidate=candidates[unit['candidate_id']];seen.add(row['unit_id'])
|
|
149
|
+
require(row.get('candidate_id')==candidate['id'] and row.get('candidate_sha256')==digest(encoded(candidate)),'Captured candidate identity differs')
|
|
150
|
+
require(row.get('source_kind')==('fixture' if plan['mode']=='fixture' else 'operator-capture'),'Capture provenance differs from plan')
|
|
151
|
+
require(row.get('status') in ('completed','connection_failed','disconnected','error','collector_error'),'Unknown capture status')
|
|
152
|
+
session=row.get('session_id')
|
|
153
|
+
if plan['protocol']!='machine-intelligibility' and row['status'] not in ('connection_failed','collector_error'):
|
|
154
|
+
identifier(session);require(session not in sessions,'Repeated session ID violates fresh-session design');sessions.add(session)
|
|
155
|
+
require(type(row.get('steps')) is list and len(row['steps'])<=len(unit['steps']),'Invalid captured step count')
|
|
156
|
+
require(len({s['step_id'] for s in row['steps']})==len(row['steps']),'Duplicate captured step')
|
|
157
|
+
wanted={s['id']:s for s in unit['steps']}
|
|
158
|
+
for s in row['steps']:
|
|
159
|
+
require(s['step_id'] in wanted and s.get('case_id')==wanted[s['step_id']]['case_id'],'Unknown captured step identity')
|
|
160
|
+
require(all(type(s.get(k)) is bool for k in ('complete','cancelled','error_present','cache_hit','interrupted')),'Invalid captured step flags')
|
|
161
|
+
require(type(s.get('audio_bytes')) is int and s['audio_bytes']>=0,'Invalid captured audio count')
|
|
162
|
+
for k in ('tts_start_ms','first_audio_ms','playout_start_ms','playout_end_ms'):number(s.get(k),k,nullable=True)
|
|
163
|
+
require(s.get('provider') is None or isinstance(s['provider'],str),'Invalid provider observation')
|
|
164
|
+
require(s.get('model') is None or isinstance(s['model'],str),'Invalid model observation')
|
|
165
|
+
result[row['unit_id']]=row
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _successful(step,candidate):
|
|
170
|
+
return step['complete'] and not step['cancelled'] and not step['error_present'] and step['audio_bytes']>0 and step.get('provider')==candidate['provider'] and step.get('model') in (None,candidate['model'])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def evaluate_session_reliability(plan,captures):
|
|
174
|
+
bound=_captures(plan,captures);result=[]
|
|
175
|
+
for c in plan['candidates']:
|
|
176
|
+
units=[u for u in plan['units'] if u['candidate_id']==c['id']];rows=[bound[u['id']] for u in units if u['id'] in bound]
|
|
177
|
+
steps=[s for r in rows for s in r['steps']]
|
|
178
|
+
complete=sum(r['status']=='completed' and len(r['steps'])==len(next(u for u in units if u['id']==r['unit_id'])['steps']) and all(_successful(s,c) for s in r['steps']) for r in rows)
|
|
179
|
+
result.append(dict(candidate_id=c['id'],sessions_planned=len(units),sessions_attempted=len(rows),captures_missing=len(units)-len(rows),
|
|
180
|
+
connections_failed=sum(r['status']=='connection_failed' for r in rows),sessions_with_synthesis=sum(bool(r['steps']) for r in rows),
|
|
181
|
+
collector_errors=sum(r['status']=='collector_error' for r in rows),
|
|
182
|
+
all_scripted_steps_completed=complete,complete_session_rate=complete/len(units) if len(rows)==len(units) else None,
|
|
183
|
+
observed_completed_fraction=complete/len(units),synthesis_attempts=len(steps),synthesis_errors=sum(s['error_present'] for s in steps),
|
|
184
|
+
zero_audio_events=sum(s['complete'] and s['audio_bytes']==0 for s in steps),fallback_or_unconfirmed_attempts=sum(s.get('provider')!=c['provider'] for s in steps),
|
|
185
|
+
model_mismatch_attempts=sum(s.get('model') is not None and s['model']!=c['model'] for s in steps),model_identity_independently_verified=False,
|
|
186
|
+
missing_synthesis_telemetry=sum(r['status'] not in ('connection_failed','collector_error') and not r['steps'] for r in rows),
|
|
187
|
+
disconnected_sessions=sum(r['status']=='disconnected' for r in rows),disconnect_attributed_to_tts=None,
|
|
188
|
+
scope='fixture protocol check' if plan['mode']=='fixture' else 'controlled benchmark sessions; not production reliability',confidence_interval=None))
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def evaluate_controlled_barge_in(plan,captures):
|
|
193
|
+
bound=_captures(plan,captures);candidates={c['id']:c for c in plan['candidates']};result=[]
|
|
194
|
+
for unit in plan['units']:
|
|
195
|
+
row=bound.get(unit['id']);steps={s['step_id']:s for s in row['steps']} if row else {};c=candidates[unit['candidate_id']]
|
|
196
|
+
long=steps.get('step:1');recovery=steps.get('step:2');trigger=row.get('trigger_ms') if row else None
|
|
197
|
+
number(trigger,'actual microphone trigger',nullable=True)
|
|
198
|
+
start=long.get('playout_start_ms') if long else None;end=long.get('playout_end_ms') if long else None
|
|
199
|
+
timing=(all(x is not None for x in (start,end,trigger)) and start<trigger<=end and
|
|
200
|
+
abs(trigger-start-plan['design']['interrupt_after_ms'])<=plan['design']['interrupt_tolerance_ms'] and
|
|
201
|
+
0<=end-trigger<=plan['design']['interrupt_tolerance_ms'])
|
|
202
|
+
mic=row.get('injected_audio_sha256')==plan['microphone']['sha256'] if row else False
|
|
203
|
+
recovered=bool(recovery and _successful(recovery,c) and trigger is not None and recovery['tts_start_ms'] is not None and recovery['tts_start_ms']>=trigger)
|
|
204
|
+
observed=bool(long and long['interrupted'] and long['audio_bytes']>0 and long.get('provider')==c['provider']
|
|
205
|
+
and long.get('model') in (None,c['model']) and timing and mic)
|
|
206
|
+
result.append(dict(unit_id=unit['id'],capture_available=row is not None,microphone_identity_matches=mic,
|
|
207
|
+
interruption_timing_valid=bool(timing),terminal_interruption_observed=bool(long and long['interrupted']),
|
|
208
|
+
recovery_completed=recovered,protocol_success=observed and recovered and row['status']=='completed',
|
|
209
|
+
interruption_to_playout_end_ms=end-trigger if timing else None,
|
|
210
|
+
subjective_latency=None,scope='fixture' if plan['mode']=='fixture' else 'frozen microphone interruption design'))
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def evaluate_rapid_turn_stress(plan,captures):
|
|
215
|
+
bound=_captures(plan,captures);candidates={c['id']:c for c in plan['candidates']};result=[]
|
|
216
|
+
for unit in plan['units']:
|
|
217
|
+
row=bound.get(unit['id']);steps={s['step_id']:s for s in row['steps']} if row else {};c=candidates[unit['candidate_id']]
|
|
218
|
+
sent=row.get('prompt_times_ms',{}) if row else {};values=[];idle=[];valid=bool(row and row['status']=='completed');previous=None
|
|
219
|
+
require(type(sent) is dict,'Invalid prompt timing observations')
|
|
220
|
+
for expected in unit['steps']:
|
|
221
|
+
s=steps.get(expected['id']);when=sent.get(expected['id']);number(when,'prompt injection time',nullable=True)
|
|
222
|
+
if s is None or not _successful(s,c) or when is None:valid=False
|
|
223
|
+
if previous is not None:
|
|
224
|
+
gap=when-previous if when is not None else None;idle.append(gap)
|
|
225
|
+
if gap is None or not 0<=gap<=plan['design']['max_idle_ms']:valid=False
|
|
226
|
+
if s:
|
|
227
|
+
a,b=s['tts_start_ms'],s['first_audio_ms']
|
|
228
|
+
clocks=(when,a,b,s['playout_start_ms'],s['playout_end_ms'])
|
|
229
|
+
if any(v is None for v in clocks) or list(clocks)!=sorted(clocks):valid=False
|
|
230
|
+
values.append(b-a if a is not None and b is not None and b>=a and not s['cache_hit'] and _successful(s,c) else None)
|
|
231
|
+
previous=s['playout_end_ms']
|
|
232
|
+
if previous is None:valid=False
|
|
233
|
+
else:values.append(None);previous=None
|
|
234
|
+
result.append(dict(unit_id=unit['id'],expected_turns=len(unit['steps']),captured_turns=len(steps),
|
|
235
|
+
timing_control_valid=valid and len(steps)==len(unit['steps']),idle_ms=idle,fresh_ttfa_ms_in_turn_order=values,
|
|
236
|
+
fitted_degradation_slope=None,confidence_interval=None,scope='fixture' if plan['mode']=='fixture' else 'timed rapid-turn campaign'))
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def evaluate_machine_intelligibility(plan,captures,references):
|
|
241
|
+
bound=_captures(plan,captures);result=[]
|
|
242
|
+
for c in plan['candidates']:
|
|
243
|
+
units=[u for u in plan['units'] if u['candidate_id']==c['id']];scores=[]
|
|
244
|
+
for unit in units:
|
|
245
|
+
row=bound.get(unit['id']);audio=unit['audio_case'];valid=bool(row and row['status']=='completed' and row.get('audio_sha256')==audio['audio']['sha256']
|
|
246
|
+
and row.get('asr_sha256')==digest(encoded(plan['asr'])) and isinstance(row.get('hypothesis'),str))
|
|
247
|
+
score=score_transcript(references[audio['id']],row['hypothesis']) if valid else None
|
|
248
|
+
if score is not None:scores.append((audio['language'],score))
|
|
249
|
+
languages=sorted({u['audio_case']['language'] for u in units});by_language={}
|
|
250
|
+
for language in languages:
|
|
251
|
+
group=[s for lg,s in scores if lg==language];expected=sum(u['audio_case']['language']==language for u in units)
|
|
252
|
+
words=sum(s['reference_words'] for s in group);chars=sum(s['reference_characters'] for s in group)
|
|
253
|
+
by_language[language]=dict(expected=expected,scored=len(group),missing_or_invalid=expected-len(group),
|
|
254
|
+
wer=sum(s['word_errors'] for s in group)/words if words and len(group)==expected and language!='ja' else None,
|
|
255
|
+
cer=sum(s['character_errors'] for s in group)/chars if chars and len(group)==expected else None,
|
|
256
|
+
silence_inserted_words=sum(s['silence_inserted_words'] for s in group),
|
|
257
|
+
structured_tokens={k:dict(matched=sum(s['structured_tokens'][k]['matched'] for s in group),reference_tokens=sum(s['structured_tokens'][k]['reference_tokens'] for s in group)) for k in (group[0]['structured_tokens'] if group else {})})
|
|
258
|
+
result.append(dict(candidate_id=c['id'],by_language=by_language,normalization='kido.nfkc-lower-punctuation.v1',
|
|
259
|
+
structured_token_scorer='kido.structured-substring-presence.v1',human_quality=None,confidence_interval=None,
|
|
260
|
+
asr_sha256=digest(encoded(plan['asr'])),scope='fixture' if plan['mode']=='fixture' else 'fixed-ASR machine intelligibility proxy'))
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _grade(plan,captures,references):
|
|
265
|
+
funcs={'session-reliability':evaluate_session_reliability,'controlled-barge-in':evaluate_controlled_barge_in,'rapid-turn-stress':evaluate_rapid_turn_stress}
|
|
266
|
+
return evaluate_machine_intelligibility(plan,captures,references) if plan['protocol']=='machine-intelligibility' else funcs[plan['protocol']](plan,captures)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def grade_speech_campaign(path,*,expected_plan_sha256,captures,output_dir):
|
|
270
|
+
root,plan,refs=_load_plan(path,expected_plan_sha256);result=_grade(plan,captures,refs)
|
|
271
|
+
invocation='speech-campaign-invocation-'+uuid.uuid4().hex
|
|
272
|
+
report=dict(schema_version=SCHEMA+'.report.v1',suite_id=SUITE_ID,invocation_id=invocation,protocol=plan['protocol'],
|
|
273
|
+
mode=plan['mode'],plan_sha256=expected_plan_sha256,results=result,real_provider_calls_made_by_grader=0,
|
|
274
|
+
captures_supplied=len(captures),captures_planned=plan['planned_units'],subjective_perceived_latency=None)
|
|
275
|
+
dest=_new_directory(output_dir);files=_write_files(dest,{'plan.json':encoded(plan),'references.json':encoded(refs),'captures.json':encoded(captures),'report.json':encoded(report)})
|
|
276
|
+
receipt=dict(schema_version=SCHEMA+'.receipt.v1',suite_id=SUITE_ID,invocation_id=invocation,files=files)
|
|
277
|
+
raw=encoded(receipt);(dest/'receipt.json').write_bytes(raw)
|
|
278
|
+
return dict(path=str(dest),suite_id=SUITE_ID,invocation_id=invocation,receipt_sha256=digest(raw),mode=plan['mode'])
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def load_speech_campaign_report(path,*,expected_receipt_sha256):
|
|
282
|
+
root=Path(path).absolute();raw=_read(root,'receipt.json');require(digest(raw)==_sha(expected_receipt_sha256),'Campaign receipt changed')
|
|
283
|
+
receipt=json_value(raw);require(receipt.get('schema_version')==SCHEMA+'.receipt.v1' and receipt.get('suite_id')==SUITE_ID and
|
|
284
|
+
set(receipt.get('files',{}))=={'plan.json','references.json','captures.json','report.json'},'Invalid campaign report census')
|
|
285
|
+
values={}
|
|
286
|
+
for p,h in receipt['files'].items():
|
|
287
|
+
b=_read(root,p);require(h==dict(sha256=digest(b),size_bytes=len(b)),'Campaign artifact changed');values[p]=json_value(b)
|
|
288
|
+
plan=values['plan.json'];report=values['report.json'];refs=values['references.json']
|
|
289
|
+
require(plan.get('schema_version')==SCHEMA+'.plan.v1' and plan.get('suite_id')==SUITE_ID and
|
|
290
|
+
report.get('schema_version')==SCHEMA+'.report.v1' and report.get('suite_id')==SUITE_ID and
|
|
291
|
+
report.get('invocation_id')==receipt.get('invocation_id') and report.get('plan_sha256')==digest(encoded(plan)) and
|
|
292
|
+
plan['files']['references.json']==dict(sha256=digest(encoded(refs)),size_bytes=len(encoded(refs))) and
|
|
293
|
+
report.get('mode')==plan['mode'] and report.get('protocol')==plan['protocol'] and
|
|
294
|
+
report.get('real_provider_calls_made_by_grader')==0 and report.get('subjective_perceived_latency') is None and
|
|
295
|
+
report.get('captures_supplied')==len(values['captures.json']) and report.get('captures_planned')==plan['planned_units'] and
|
|
296
|
+
report.get('results')==_grade(plan,values['captures.json'],refs),'Campaign report calculations or scope changed')
|
|
297
|
+
return report
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def run_fixture_speech_campaign(path,*,expected_plan_sha256,output_dir):
|
|
301
|
+
_,plan,refs=_load_plan(path,expected_plan_sha256);require(plan['mode']=='fixture','Fixtures cannot populate a real capture campaign')
|
|
302
|
+
candidates={c['id']:c for c in plan['candidates']};rows=[]
|
|
303
|
+
for unit in plan['units']:
|
|
304
|
+
c=candidates[unit['candidate_id']];steps=[];sent={};clock=0
|
|
305
|
+
for i,expected in enumerate(unit['steps']):
|
|
306
|
+
sent[expected['id']]=clock;start=clock+10
|
|
307
|
+
end=start+100 if plan['protocol']!='controlled-barge-in' or i else start+plan['design']['interrupt_after_ms']+10
|
|
308
|
+
steps.append(dict(step_id=expected['id'],case_id=expected['case_id'],provider=c['provider'],model=c['model'],audio_bytes=32,
|
|
309
|
+
complete=True,cancelled=False,error_present=False,cache_hit=False,interrupted=plan['protocol']=='controlled-barge-in' and i==0,
|
|
310
|
+
tts_start_ms=clock,first_audio_ms=start,playout_start_ms=start,playout_end_ms=end))
|
|
311
|
+
clock=end+min(10,plan['design']['max_idle_ms'])
|
|
312
|
+
row=dict(unit_id=unit['id'],candidate_id=c['id'],candidate_sha256=digest(encoded(c)),source_kind='fixture',
|
|
313
|
+
status='completed',session_id='fixture:'+unit['id'],steps=steps,prompt_times_ms=sent)
|
|
314
|
+
if plan['protocol']=='controlled-barge-in':row.update(trigger_ms=steps[0]['playout_start_ms']+plan['design']['interrupt_after_ms'],injected_audio_sha256=plan['microphone']['sha256'])
|
|
315
|
+
if plan['protocol']=='machine-intelligibility':
|
|
316
|
+
a=unit['audio_case'];row.update(audio_sha256=a['audio']['sha256'],asr_sha256=digest(encoded(plan['asr'])),hypothesis='fixture transcript')
|
|
317
|
+
rows.append(row)
|
|
318
|
+
return grade_speech_campaign(path,expected_plan_sha256=expected_plan_sha256,captures=rows,output_dir=output_dir)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def run_speech_campaign(path,*,expected_plan_sha256,collector,output_dir,allow_external_calls=False):
|
|
322
|
+
"""Run a deliberately supplied, source-hashed trusted-local collector.
|
|
323
|
+
|
|
324
|
+
The collector owns provider/session integration, dependency review and
|
|
325
|
+
request deadlines. This is not an OS sandbox or a provider implementation.
|
|
326
|
+
No collector is imported or discovered by the SDK. Input views omit gold.
|
|
327
|
+
"""
|
|
328
|
+
import inspect
|
|
329
|
+
root,plan,_=_load_plan(path,expected_plan_sha256)
|
|
330
|
+
require(plan['mode']=='capture' and type(allow_external_calls) is bool and allow_external_calls,
|
|
331
|
+
'Explicit external campaign launch is required')
|
|
332
|
+
require(callable(collector) and plan.get('collector_source_sha256') is not None,'Freeze the reviewed collector source hash')
|
|
333
|
+
file=inspect.getsourcefile(collector);require(file is not None,'Collector source must be inspectable')
|
|
334
|
+
file=Path(file).absolute();require(digest(_read(file.parent,file.name))==plan['collector_source_sha256'],'Collector source differs from reviewed plan')
|
|
335
|
+
requests=speech_campaign_requests(root,expected_plan_sha256=expected_plan_sha256)
|
|
336
|
+
dest=_new_directory(output_dir);candidates={c['id']:c for c in plan['candidates']};captures=[]
|
|
337
|
+
for i,unit in enumerate(plan['units']):
|
|
338
|
+
assets=dest/'collector-inputs'/('unit-'+str(i+1)+'-assets');assets.mkdir(parents=True)
|
|
339
|
+
names=[unit['audio_case']['audio_file']] if 'audio_case' in unit else ['audio/microphone.wav'] if plan['microphone'] else []
|
|
340
|
+
files={p:requests['input_files'][p] for p in names}
|
|
341
|
+
_write_files(assets,{p:_read(root,p) for p in files})
|
|
342
|
+
request=dict(protocol=plan['protocol'],plan_sha256=expected_plan_sha256,unit=unit,
|
|
343
|
+
candidate=candidates[unit['candidate_id']],design=plan['design'],asr=plan['asr'],
|
|
344
|
+
microphone=plan['microphone'],input_files=files,input_root=str(assets))
|
|
345
|
+
_write_files(dest,{'collector-inputs/unit-'+str(i+1)+'.json':encoded(request)})
|
|
346
|
+
try:row=collector(request)
|
|
347
|
+
except Exception:
|
|
348
|
+
row=dict(unit_id=unit['id'],candidate_id=unit['candidate_id'],candidate_sha256=digest(encoded(candidates[unit['candidate_id']])),
|
|
349
|
+
source_kind='operator-capture',status='collector_error',session_id=None,steps=[])
|
|
350
|
+
_captures(plan,[row]);captures.append(row)
|
|
351
|
+
_write_files(dest,{'collector-captures/unit-'+str(i+1)+'.json':encoded(row)})
|
|
352
|
+
return grade_speech_campaign(root,expected_plan_sha256=expected_plan_sha256,captures=captures,output_dir=dest/'evaluation')
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def main(argv=None):
|
|
356
|
+
p=argparse.ArgumentParser(description=__doc__);commands=p.add_subparsers(dest='command',required=True)
|
|
357
|
+
commands.add_parser('describe')
|
|
358
|
+
c=commands.add_parser('freeze');c.add_argument('--bundle',required=True);c.add_argument('--expected-manifest-sha256',required=True);c.add_argument('--design-json',required=True);c.add_argument('--output',required=True)
|
|
359
|
+
for name in ('requests','fixture','grade'):
|
|
360
|
+
c=commands.add_parser(name);c.add_argument('--plan',required=True);c.add_argument('--expected-plan-sha256',required=True)
|
|
361
|
+
if name!='requests':c.add_argument('--output',required=True)
|
|
362
|
+
if name=='grade':c.add_argument('--captures-json',required=True)
|
|
363
|
+
args=p.parse_args(argv)
|
|
364
|
+
try:
|
|
365
|
+
if args.command=='describe':result=dict(suite_id=SUITE_ID,protocols=PROTOCOLS,provider_calls=False,live_collector='separately reviewed external capture')
|
|
366
|
+
elif args.command=='freeze':
|
|
367
|
+
from .speech_complete import load_kido_complete
|
|
368
|
+
b=load_kido_complete(args.bundle,expected_manifest_sha256=args.expected_manifest_sha256)
|
|
369
|
+
result=freeze_speech_campaign(b,output_dir=args.output,**json_value(_read(Path(args.design_json).absolute().parent,Path(args.design_json).name)))
|
|
370
|
+
elif args.command=='requests':result=speech_campaign_requests(args.plan,expected_plan_sha256=args.expected_plan_sha256)
|
|
371
|
+
elif args.command=='fixture':result=run_fixture_speech_campaign(args.plan,expected_plan_sha256=args.expected_plan_sha256,output_dir=args.output)
|
|
372
|
+
else:result=grade_speech_campaign(args.plan,expected_plan_sha256=args.expected_plan_sha256,output_dir=args.output,
|
|
373
|
+
captures=json_value(_read(Path(args.captures_json).absolute().parent,Path(args.captures_json).name)))
|
|
374
|
+
except (ValueError,OSError,KeyError,TypeError):p.exit(2,'Speech campaign failed; check frozen design, capture identities and output.\n')
|
|
375
|
+
print(json.dumps(result,indent=2,ensure_ascii=True))
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Exhaustive Kido benchmark catalog and retained-evidence evaluation.
|
|
2
|
+
|
|
3
|
+
Post-0.5.0 source only. Separate invocation; no provider or imported-code runs.
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import argparse
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from ._speech_kido import digest,encoded,json_value,require,csv_rows
|
|
11
|
+
from . import _speech_complete_source as source
|
|
12
|
+
from ._speech_complete_scoring import (evaluate_google_direct_api,evaluate_pronunciation_proxy,
|
|
13
|
+
evaluate_isolated_diagnostics,evaluate_listening_diagnostics,evaluate_pt_br_categories,evaluate_official_turns,evaluate_stt_diagnostics)
|
|
14
|
+
from ._speech_benchmark_scoring import grade_records
|
|
15
|
+
from .speech_evidence import _new_directory,_read,_write_files,_sha,_grade_rows
|
|
16
|
+
|
|
17
|
+
SUITE_ID='speech.kido-complete.v1'
|
|
18
|
+
SCHEMA='sphynx.speech.kido-complete'
|
|
19
|
+
MODULES=('_speech_complete_source.py','_speech_complete_scoring.py','speech_complete.py',
|
|
20
|
+
'_speech_kido.py','_speech_scoring.py','speech_evidence.py','_speech_benchmark_sources.py','_speech_benchmark_scoring.py','speech_contracts.py')
|
|
21
|
+
LIMITS=dict(historical_only=True,real_provider_calls=0,hosted_admitted=False,
|
|
22
|
+
population_inference=False,new_waveform_quality=False,subjective_perceived_latency=False)
|
|
23
|
+
SOURCE_FAMILIES={
|
|
24
|
+
'results/human-eval/':'blind_quality',
|
|
25
|
+
'results/isolated/official-v1/':'isolated_synthesis',
|
|
26
|
+
'results/isolated/multilingual-extension-v1/':'multilingual_isolated',
|
|
27
|
+
'results/live-agent/official-v1/':'live_agent_synthesis',
|
|
28
|
+
'results/live-agent/reliability-v1/':'session_reliability',
|
|
29
|
+
'results/live-agent/pronunciation-v1/':'pronunciation',
|
|
30
|
+
'results/live-agent/language-switching-v1/':'language_switching',
|
|
31
|
+
'results/exploratory/cartesia-ink-2-stt/':'stt_recognition',
|
|
32
|
+
'results/exploratory/google-gemini-tts/':'google_direct_api',
|
|
33
|
+
'results/exploratory/pronunciation-v1-cartesia-only/':'pronunciation_proxy',
|
|
34
|
+
}
|
|
35
|
+
# Every methodology document has an explicit home. A plan is not an executed study.
|
|
36
|
+
CATALOG=(
|
|
37
|
+
('isolated_synthesis','saved-evidence','isolated-benchmark.md'),
|
|
38
|
+
('blind_quality','saved-evidence','human-eval.md'),
|
|
39
|
+
('pt_br_listening','saved-evidence','pt-br-listening-pack.md'),
|
|
40
|
+
('live_agent_synthesis','saved-evidence','live-agent.md'),
|
|
41
|
+
('stt_recognition','saved-evidence','cartesia-ink2-exploratory.md'),
|
|
42
|
+
('stt_diagnostics','saved-evidence','cartesia-ink2-exploratory.md'),
|
|
43
|
+
('stt_turn_lifecycle','saved-evidence','live-telemetry-schema.md'),
|
|
44
|
+
('pronunciation','saved-evidence','pronunciation.md'),
|
|
45
|
+
('language_switching','saved-evidence','language-switching.md'),
|
|
46
|
+
('voice_continuity','saved-evidence','language-switching.md'),
|
|
47
|
+
('code_switching','saved-evidence','multilingual.md'),
|
|
48
|
+
('playout_interruptions','saved-evidence','barge-in-and-rapid-turns.md'),
|
|
49
|
+
('google_direct_api','saved-evidence','google-direct-api-extension.md'),
|
|
50
|
+
('pronunciation_proxy','saved-evidence','pronunciation.md'),
|
|
51
|
+
('isolated_diagnostics','saved-evidence','percentile-conventions.md'),
|
|
52
|
+
('listening_diagnostics','saved-evidence','corpus.md'),
|
|
53
|
+
('pt_br_categories','saved-evidence','pt-br-listening-pack.md'),
|
|
54
|
+
('official_turn_diagnostics','saved-evidence','barge-in-and-rapid-turns.md'),
|
|
55
|
+
('multilingual_isolated','campaign','multilingual.md'),
|
|
56
|
+
('session_reliability','campaign','reliability.md'),
|
|
57
|
+
('machine_intelligibility','campaign','intelligibility.md'),
|
|
58
|
+
('controlled_barge_in','campaign','barge-in-and-rapid-turns.md'),
|
|
59
|
+
('rapid_turn_stress','campaign','barge-in-and-rapid-turns.md'),
|
|
60
|
+
('third_party_stt_research','reference-only','cartesia-ink2-exploratory.md'),
|
|
61
|
+
('pronunciation_customization','capability-only','pronunciation.md'),
|
|
62
|
+
('protocol_overview','reference-only','protocol.md'),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_kido_evaluations():
|
|
67
|
+
return dict(suite_id=SUITE_ID,source_commit=source.SOURCE_COMMIT,source_lock_sha256=source.SOURCE_PIN,
|
|
68
|
+
invocation_schema=SCHEMA+'.invocation.v1',catalog=[dict(id=k,surface=s,methodology='methodology/'+p) for k,s,p in CATALOG],
|
|
69
|
+
source_families=SOURCE_FAMILIES,
|
|
70
|
+
execution='retained evidence only; campaign execution has a separate invocation',
|
|
71
|
+
implementation_sha256={name:digest(Path(__file__).with_name(name).read_bytes()) for name in MODULES},limits=LIMITS)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def export_kido_complete_source(repository,output_dir):
|
|
75
|
+
lock,data=source.git_bytes(repository);dest=_new_directory(output_dir);_write_files(dest,data)
|
|
76
|
+
return dict(path=str(dest),source_commit=source.SOURCE_COMMIT,source_lock_sha256=digest(encoded(lock)),files=len(data),source_executed=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def import_kido_complete(source_path,output_dir):
|
|
80
|
+
lock,data=source.source_bytes(source_path);evidence=source.normalize(data)
|
|
81
|
+
evidence['isolated_trials']=csv_rows(data['results/isolated/official-v1/combined/trials.csv'])
|
|
82
|
+
methodologies={p for p in data if p.startswith('methodology/') and p.endswith('.md')}
|
|
83
|
+
require(methodologies<={'methodology/'+p for _,_,p in CATALOG},'Uncatalogued methodology document')
|
|
84
|
+
require(all(any(p.startswith(prefix) for prefix in SOURCE_FAMILIES) or p in
|
|
85
|
+
('results/live-agent/README.md','results/README.md') for p in data if p.startswith('results/')),
|
|
86
|
+
'Uncatalogued result family')
|
|
87
|
+
dest=_new_directory(output_dir);files=_write_files(dest,{'source/'+p:b for p,b in data.items()}|{'evidence.json':encoded(evidence)})
|
|
88
|
+
manifest=dict(schema_version=SCHEMA+'.bundle.v1',suite_id=SUITE_ID,source_lock_sha256=digest(encoded(lock)),
|
|
89
|
+
source_commit=source.SOURCE_COMMIT,files=files,provider_called=False,candidate_executed=False,hosted_admitted=False)
|
|
90
|
+
raw=encoded(manifest);(dest/'manifest.json').write_bytes(raw)
|
|
91
|
+
return dict(path=str(dest),manifest_sha256=digest(raw),suite_id=SUITE_ID,source_files=len(data))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class KidoComplete:
|
|
96
|
+
path:Path
|
|
97
|
+
manifest_sha256:str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _verified(bundle):
|
|
101
|
+
raw=_read(bundle.path,'manifest.json');require(digest(raw)==bundle.manifest_sha256,'Complete manifest changed')
|
|
102
|
+
manifest=json_value(raw)
|
|
103
|
+
require(manifest.get('schema_version')==SCHEMA+'.bundle.v1' and manifest.get('suite_id')==SUITE_ID and
|
|
104
|
+
manifest.get('source_lock_sha256')==source.SOURCE_PIN and manifest.get('source_commit')==source.SOURCE_COMMIT,
|
|
105
|
+
'Not the pinned complete benchmark source')
|
|
106
|
+
_,data=source.source_bytes(bundle.path/'source')
|
|
107
|
+
require(set(manifest.get('files',{}))=={'source/'+p for p in data}|{'evidence.json'},'Complete file census changed')
|
|
108
|
+
for p,h in manifest['files'].items():
|
|
109
|
+
b=_read(bundle.path,p);require(h==dict(sha256=digest(b),size_bytes=len(b)),'Complete artifact changed')
|
|
110
|
+
evidence=source.normalize(data);evidence['isolated_trials']=csv_rows(data['results/isolated/official-v1/combined/trials.csv'])
|
|
111
|
+
require(_read(bundle.path,'evidence.json')==encoded(evidence),'Complete evidence differs from source')
|
|
112
|
+
require(all(manifest.get(k) is False for k in ('provider_called','candidate_executed','hosted_admitted')),'Complete execution boundary changed')
|
|
113
|
+
return evidence
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def load_kido_complete(path,*,expected_manifest_sha256):
|
|
117
|
+
bundle=KidoComplete(Path(path).absolute(),_sha(expected_manifest_sha256));_verified(bundle);return bundle
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _results(evidence):
|
|
121
|
+
saved=evidence['saved'];summary,cases=_grade_rows(saved['observations'],saved['references'])
|
|
122
|
+
pt=[r for r in evidence['benchmarks']['observations'] if r['track']=='pt_br_listening']
|
|
123
|
+
return dict(saved_evidence=dict(summaries=summary,per_case=cases,audit=saved['audit']),
|
|
124
|
+
benchmarks=grade_records(evidence['benchmarks']),google_direct_api=evaluate_google_direct_api(evidence['google']),
|
|
125
|
+
pronunciation_proxy=evaluate_pronunciation_proxy(evidence['proxy']),
|
|
126
|
+
isolated_diagnostics=evaluate_isolated_diagnostics(evidence['isolated_trials']),
|
|
127
|
+
listening_diagnostics=evaluate_listening_diagnostics(saved['observations'],saved['references'],evidence['isolated_trials']),
|
|
128
|
+
pt_br_categories=evaluate_pt_br_categories(pt),official_turn_diagnostics=evaluate_official_turns(evidence['official_sessions']),
|
|
129
|
+
stt_diagnostics=evaluate_stt_diagnostics(evidence['stt_analyses']),
|
|
130
|
+
unexecuted_campaigns=[k for k,s,_ in CATALOG if s=='campaign'],
|
|
131
|
+
source_documents=evidence['source_documents'],
|
|
132
|
+
nonexperimental_references=[k for k,s,_ in CATALOG if s in ('reference-only','capability-only')])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def grade_kido_complete(bundle,output_dir):
|
|
136
|
+
evidence=_verified(bundle);definition=list_kido_evaluations()
|
|
137
|
+
invocation=dict(schema_version=SCHEMA+'.invocation.v1',id='speech-complete-invocation-'+uuid.uuid4().hex,
|
|
138
|
+
suite_id=SUITE_ID,bundle_manifest_sha256=bundle.manifest_sha256,definition=definition,provider_called=False,candidate_executed=False)
|
|
139
|
+
report=dict(schema_version=SCHEMA+'.report.v1',suite_id=SUITE_ID,invocation_id=invocation['id'],
|
|
140
|
+
invocation_sha256=digest(encoded(invocation)),results=_results(evidence),limits=LIMITS)
|
|
141
|
+
dest=_new_directory(output_dir);files=_write_files(dest,{'evidence.json':encoded(evidence),'invocation.json':encoded(invocation),'report.json':encoded(report)})
|
|
142
|
+
receipt=dict(schema_version=SCHEMA+'.receipt.v1',suite_id=SUITE_ID,invocation_id=invocation['id'],files=files)
|
|
143
|
+
raw=encoded(receipt);(dest/'receipt.json').write_bytes(raw)
|
|
144
|
+
return dict(path=str(dest),receipt_sha256=digest(raw),invocation_id=invocation['id'],suite_id=SUITE_ID)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_kido_complete_report(path,*,expected_receipt_sha256):
|
|
148
|
+
root=Path(path).absolute();raw=_read(root,'receipt.json')
|
|
149
|
+
require(digest(raw)==_sha(expected_receipt_sha256),'Complete report receipt changed');receipt=json_value(raw)
|
|
150
|
+
require(receipt.get('schema_version')==SCHEMA+'.receipt.v1' and receipt.get('suite_id')==SUITE_ID and
|
|
151
|
+
set(receipt.get('files',{}))=={'evidence.json','invocation.json','report.json'},'Not a complete benchmark report')
|
|
152
|
+
payload={}
|
|
153
|
+
for p,h in receipt['files'].items():
|
|
154
|
+
b=_read(root,p);require(h==dict(sha256=digest(b),size_bytes=len(b)),'Complete report artifact changed');payload[p]=json_value(b)
|
|
155
|
+
invocation=payload['invocation.json'];report=payload['report.json']
|
|
156
|
+
require(invocation.get('schema_version')==SCHEMA+'.invocation.v1' and report.get('schema_version')==SCHEMA+'.report.v1' and
|
|
157
|
+
invocation.get('suite_id')==report.get('suite_id')==SUITE_ID and invocation.get('id')==report.get('invocation_id')==receipt.get('invocation_id') and
|
|
158
|
+
report.get('invocation_sha256')==digest(encoded(invocation)) and invocation.get('provider_called') is False and invocation.get('candidate_executed') is False,
|
|
159
|
+
'Complete invocation identity or execution boundary changed')
|
|
160
|
+
definition=invocation.get('definition',{})
|
|
161
|
+
require(definition.get('source_lock_sha256')==source.SOURCE_PIN and definition.get('source_commit')==source.SOURCE_COMMIT
|
|
162
|
+
and definition.get('catalog')==list_kido_evaluations()['catalog'] and definition.get('source_families')==SOURCE_FAMILIES
|
|
163
|
+
and definition.get('limits')==LIMITS,'Complete source or catalog changed')
|
|
164
|
+
require(set(definition.get('implementation_sha256',{}))==set(MODULES),'Missing complete implementation identities')
|
|
165
|
+
for v in definition['implementation_sha256'].values():_sha(v)
|
|
166
|
+
require(report.get('limits')==LIMITS and report.get('results')==_results(payload['evidence.json']),'Complete calculations or limits changed')
|
|
167
|
+
return report
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def complete_tts_cases(bundle,*,benchmark,case_ids=None,profile=None,language_hint=None):
|
|
171
|
+
"""Input-only cases for existing reviewed candidate execution or campaign plans."""
|
|
172
|
+
evidence=_verified(bundle);cases=[]
|
|
173
|
+
if benchmark=='isolated-official':
|
|
174
|
+
unique={}
|
|
175
|
+
for r in evidence['saved']['observations']:
|
|
176
|
+
if r['track']!='tts_isolated':continue
|
|
177
|
+
value=dict(id='isolated:'+r['case_id'],text=r['input_text'],language=r['language'])
|
|
178
|
+
require(value['id'] not in unique or unique[value['id']]==value,'Conflicting original corpus identity')
|
|
179
|
+
unique[value['id']]=value
|
|
180
|
+
cases=sorted(unique.values(),key=lambda r:r['id'])
|
|
181
|
+
elif benchmark=='official-live-script':
|
|
182
|
+
cases=[{k:r[k] for k in ('id','text','language')} for r in evidence['official_script']]
|
|
183
|
+
elif benchmark=='multilingual-isolated':
|
|
184
|
+
rows=evidence['multilingual']['lines']
|
|
185
|
+
support={'elevenlabs':{'en','pt-BR','es','fr','ja'},'cartesia':{'en','pt-BR','es','fr','ja'},
|
|
186
|
+
'minimax':{'en','pt-BR','es','fr','ja'},'rime':{'en','es','fr'},'deepgram':{'en'}}
|
|
187
|
+
require(profile is None or profile in support,'No frozen language support matrix for this profile')
|
|
188
|
+
cases=[dict(id='multilingual:'+r['id'],text=r['text'],language=r['language']) for r in rows if profile is None or r['language'] in support[profile]]
|
|
189
|
+
elif benchmark=='google-direct-api':
|
|
190
|
+
cases=[dict(id='google-probe:'+k,text=v,language='pt-BR' if k in ('short_pt','long_pt','numbers_pt','mixed_pt_en') else 'en') for k,v in evidence['google_texts'].items()]
|
|
191
|
+
elif benchmark=='pronunciation-proxy':
|
|
192
|
+
cases=[dict(id='pronunciation-proxy:'+r['id'],text=r['reference'],language='en') for r in evidence['proxy']]
|
|
193
|
+
else:
|
|
194
|
+
require(benchmark in ('pt-br-listening','pronunciation','code-switching'),'Unknown complete TTS corpus')
|
|
195
|
+
cases=[dict(r,id=r['id'].replace('/',':',1)) for r in evidence['benchmarks']['cases'] if r['id'].startswith(benchmark+'/')]
|
|
196
|
+
require(profile is None or benchmark=='multilingual-isolated','Profile filtering applies only to frozen multilingual support')
|
|
197
|
+
if benchmark=='code-switching':
|
|
198
|
+
import re
|
|
199
|
+
require(isinstance(language_hint,str) and re.fullmatch('[a-z]{2}',language_hint),'Select an explicit mixed-language hint')
|
|
200
|
+
for row in cases:row['language']=language_hint
|
|
201
|
+
else:require(language_hint is None,'Native corpus language cannot be overridden')
|
|
202
|
+
from .speech_contracts import identifier
|
|
203
|
+
for row in cases:identifier(row['id'])
|
|
204
|
+
if case_ids is not None:
|
|
205
|
+
require(type(case_ids) is list and len(set(case_ids))==len(case_ids) and set(case_ids)<={r['id'] for r in cases},'Invalid complete case selection')
|
|
206
|
+
cases=[r for r in cases if r['id'] in case_ids]
|
|
207
|
+
require(cases,'Empty complete case selection')
|
|
208
|
+
return dict(cases=cases,cohort_identity=dict(kind=SUITE_ID+':'+benchmark,sha256=digest(encoded(dict(bundle=bundle.manifest_sha256,cases=cases)))),
|
|
209
|
+
suite_id=SUITE_ID,new_waveform_ratings_available=False,adaptation='TTS contract PCM16 16k; not historical transport parity')
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(argv=None):
|
|
213
|
+
import json
|
|
214
|
+
parser=argparse.ArgumentParser(description=__doc__);commands=parser.add_subparsers(dest='command',required=True)
|
|
215
|
+
commands.add_parser('describe')
|
|
216
|
+
c=commands.add_parser('export-source');c.add_argument('--repository',required=True);c.add_argument('--output',required=True)
|
|
217
|
+
c=commands.add_parser('import-kido');c.add_argument('--source',required=True);c.add_argument('--output',required=True)
|
|
218
|
+
for name in ('grade','tts-cases'):
|
|
219
|
+
c=commands.add_parser(name);c.add_argument('--bundle',required=True);c.add_argument('--expected-manifest-sha256',required=True)
|
|
220
|
+
if name=='grade':c.add_argument('--output',required=True)
|
|
221
|
+
else:
|
|
222
|
+
c.add_argument('--benchmark',required=True);c.add_argument('--case-id',action='append');c.add_argument('--profile');c.add_argument('--language-hint')
|
|
223
|
+
args=parser.parse_args(argv)
|
|
224
|
+
try:
|
|
225
|
+
if args.command=='describe':result=list_kido_evaluations()
|
|
226
|
+
elif args.command=='export-source':result=export_kido_complete_source(args.repository,args.output)
|
|
227
|
+
elif args.command=='import-kido':result=import_kido_complete(args.source,args.output)
|
|
228
|
+
else:
|
|
229
|
+
bundle=load_kido_complete(args.bundle,expected_manifest_sha256=args.expected_manifest_sha256)
|
|
230
|
+
result=grade_kido_complete(bundle,args.output) if args.command=='grade' else complete_tts_cases(bundle,
|
|
231
|
+
benchmark=args.benchmark,case_ids=args.case_id,profile=args.profile,language_hint=args.language_hint)
|
|
232
|
+
except (ValueError,OSError,KeyError,TypeError):parser.exit(2,'Complete speech operation failed; check source pin, selection and output.\n')
|
|
233
|
+
print(json.dumps(result,indent=2,ensure_ascii=True))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__=='__main__':main()
|