rtpbench 0.3.0__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.
- rtpbench/.dockerignore +4 -0
- rtpbench/Dockerfile +12 -0
- rtpbench/__init__.py +6 -0
- rtpbench/__main__.py +3 -0
- rtpbench/agent.py +746 -0
- rtpbench/analyze.py +520 -0
- rtpbench/bargein.py +272 -0
- rtpbench/bench.py +1069 -0
- rtpbench/caller.py +781 -0
- rtpbench/capture.py +33 -0
- rtpbench/carrier/compose.yaml +68 -0
- rtpbench/carrier/opensips.cfg +226 -0
- rtpbench/carrier/tls.cfg +25 -0
- rtpbench/carrier/tls.yaml +9 -0
- rtpbench/cases.py +1989 -0
- rtpbench/check.py +369 -0
- rtpbench/cli.py +616 -0
- rtpbench/clock.py +174 -0
- rtpbench/compose.yaml +55 -0
- rtpbench/corpus/README.md +55 -0
- rtpbench/corpus/cough-c1-0.wav +0 -0
- rtpbench/corpus/cough-c1-1.wav +0 -0
- rtpbench/corpus/cough-c2-0.wav +0 -0
- rtpbench/corpus/cough-c2-1.wav +0 -0
- rtpbench/corpus/cough-man-0.wav +0 -0
- rtpbench/corpus/cough-man-1.wav +0 -0
- rtpbench/corpus/cough-short-0.wav +0 -0
- rtpbench/corpus/cough-short-1.wav +0 -0
- rtpbench/corpus/cough-woman_three_times-0.wav +0 -0
- rtpbench/corpus/human-en-dr-0.wav +0 -0
- rtpbench/corpus/human-en-dr-1.wav +0 -0
- rtpbench/corpus/human-en-sc-0.wav +0 -0
- rtpbench/corpus/human-en-sc-1.wav +0 -0
- rtpbench/corpus/human-en-sc-2.wav +0 -0
- rtpbench/corpus/human-es-f012-0.wav +0 -0
- rtpbench/corpus/human-es-f012-1.wav +0 -0
- rtpbench/corpus/human-es-f012-2.wav +0 -0
- rtpbench/corpus/human-es-f016-0.wav +0 -0
- rtpbench/corpus/human-es-f016-1.wav +0 -0
- rtpbench/corpus/human-es-f016-2.wav +0 -0
- rtpbench/corpus/lines/en/interrupt-00.wav +0 -0
- rtpbench/corpus/lines/en/interrupt-01.wav +0 -0
- rtpbench/corpus/lines/en/interrupt-02.wav +0 -0
- rtpbench/corpus/lines/en/interrupt-03.wav +0 -0
- rtpbench/corpus/lines/en/prompt-00.wav +0 -0
- rtpbench/corpus/lines/en/prompt-01.wav +0 -0
- rtpbench/corpus/lines/en/prompt-02.wav +0 -0
- rtpbench/corpus/lines/en/prompt-03.wav +0 -0
- rtpbench/corpus/lines/en/prompt-04.wav +0 -0
- rtpbench/corpus/lines/en/prompt-05.wav +0 -0
- rtpbench/corpus/lines/en/prompt-06.wav +0 -0
- rtpbench/corpus/lines/en/prompt-07.wav +0 -0
- rtpbench/corpus/lines/en/prompt-08.wav +0 -0
- rtpbench/corpus/lines/en/prompt-09.wav +0 -0
- rtpbench/corpus/lines/en/prompt.wav +0 -0
- rtpbench/corpus/lines/en/transfer.wav +0 -0
- rtpbench/corpus/lines/en/turn-00.wav +0 -0
- rtpbench/corpus/lines/en/turn-01.wav +0 -0
- rtpbench/corpus/lines/en/turn-02.wav +0 -0
- rtpbench/corpus/lines/en/turn-03.wav +0 -0
- rtpbench/corpus/lines/en/turn-04.wav +0 -0
- rtpbench/corpus/lines/en/turn-05.wav +0 -0
- rtpbench/corpus/lines/en/turn-06.wav +0 -0
- rtpbench/corpus/lines/en/turn-07.wav +0 -0
- rtpbench/corpus/lines/en/turn-08.wav +0 -0
- rtpbench/corpus/lines/en/turn-09.wav +0 -0
- rtpbench/corpus/lines/en/turn-10.wav +0 -0
- rtpbench/corpus/lines/en/turn-11.wav +0 -0
- rtpbench/corpus/tts-ald-bc.wav +0 -0
- rtpbench/corpus/tts-ald-long.wav +0 -0
- rtpbench/corpus/tts-ald-s03.wav +0 -0
- rtpbench/corpus/tts-ald-s07.wav +0 -0
- rtpbench/corpus/tts-ald-s11.wav +0 -0
- rtpbench/corpus/tts-cori-bc.wav +0 -0
- rtpbench/corpus/tts-cori-long.wav +0 -0
- rtpbench/corpus/tts-cori-s01.wav +0 -0
- rtpbench/corpus/tts-cori-s05.wav +0 -0
- rtpbench/corpus/tts-cori-s09.wav +0 -0
- rtpbench/corpus/tts-davefx-bc.wav +0 -0
- rtpbench/corpus/tts-davefx-long.wav +0 -0
- rtpbench/corpus/tts-davefx-s00.wav +0 -0
- rtpbench/corpus/tts-davefx-s04.wav +0 -0
- rtpbench/corpus/tts-davefx-s08.wav +0 -0
- rtpbench/corpus/tts-libritts0-bc.wav +0 -0
- rtpbench/corpus/tts-libritts0-long.wav +0 -0
- rtpbench/corpus/tts-libritts0-s02.wav +0 -0
- rtpbench/corpus/tts-libritts0-s06.wav +0 -0
- rtpbench/corpus/tts-libritts0-s10.wav +0 -0
- rtpbench/corpus/tts-libritts3-bc.wav +0 -0
- rtpbench/corpus/tts-libritts3-long.wav +0 -0
- rtpbench/corpus/tts-libritts3-s03.wav +0 -0
- rtpbench/corpus/tts-libritts3-s07.wav +0 -0
- rtpbench/corpus/tts-libritts3-s11.wav +0 -0
- rtpbench/corpus/tts-ljspeech-bc.wav +0 -0
- rtpbench/corpus/tts-ljspeech-long.wav +0 -0
- rtpbench/corpus/tts-ljspeech-s00.wav +0 -0
- rtpbench/corpus/tts-ljspeech-s04.wav +0 -0
- rtpbench/corpus/tts-ljspeech-s08.wav +0 -0
- rtpbench/corpus/tts-sharvardf-bc.wav +0 -0
- rtpbench/corpus/tts-sharvardf-long.wav +0 -0
- rtpbench/corpus/tts-sharvardf-s02.wav +0 -0
- rtpbench/corpus/tts-sharvardf-s06.wav +0 -0
- rtpbench/corpus/tts-sharvardf-s10.wav +0 -0
- rtpbench/corpus/tts-sharvardm-bc.wav +0 -0
- rtpbench/corpus/tts-sharvardm-long.wav +0 -0
- rtpbench/corpus/tts-sharvardm-s01.wav +0 -0
- rtpbench/corpus/tts-sharvardm-s05.wav +0 -0
- rtpbench/corpus/tts-sharvardm-s09.wav +0 -0
- rtpbench/detect.py +145 -0
- rtpbench/g711.py +82 -0
- rtpbench/mock/compose.yaml +18 -0
- rtpbench/pcap.py +106 -0
- rtpbench/progress.py +184 -0
- rtpbench/realtime.py +1015 -0
- rtpbench/report.py +450 -0
- rtpbench/rtp.py +166 -0
- rtpbench/sip.py +653 -0
- rtpbench/speech.py +100 -0
- rtpbench/speechcal.py +367 -0
- rtpbench/srtp.py +191 -0
- rtpbench/targets/aivc/aivc.ini +25 -0
- rtpbench/targets/aivc/compose.yaml +72 -0
- rtpbench/targets/aivc/constraints.txt +52 -0
- rtpbench/targets/aivc/opensips.cfg +45 -0
- rtpbench/targets/ava/ai-agent.yaml +275 -0
- rtpbench/targets/ava/ari.conf +7 -0
- rtpbench/targets/ava/compose.yaml +128 -0
- rtpbench/targets/ava/constraints.txt +75 -0
- rtpbench/targets/ava/extensions.conf +10 -0
- rtpbench/targets/ava/http.conf +4 -0
- rtpbench/targets/ava/pjsip.conf +19 -0
- rtpbench/targets/ava/websocket_client.conf +15 -0
- rtpbench/targets/livekit/agent.py +111 -0
- rtpbench/targets/livekit/compose.yaml +86 -0
- rtpbench/targets/livekit/constraints.txt +72 -0
- rtpbench/tls.py +284 -0
- rtpbench/uri/advertise.yaml +7 -0
- rtpbench/uri/network.yaml +14 -0
- rtpbench/voice.py +18 -0
- rtpbench-0.3.0.dist-info/METADATA +123 -0
- rtpbench-0.3.0.dist-info/RECORD +144 -0
- rtpbench-0.3.0.dist-info/WHEEL +4 -0
- rtpbench-0.3.0.dist-info/entry_points.txt +2 -0
- rtpbench-0.3.0.dist-info/licenses/LICENSE +202 -0
rtpbench/bargein.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Barge-in: the shared script, and stop time from the caller's capture.
|
|
2
|
+
|
|
3
|
+
Each turn the caller speaks a prompt, waits for the agent's reply to start, then cuts in at a
|
|
4
|
+
scripted point with an interruption, a backchannel or a cough. The reference agent knows the
|
|
5
|
+
same script (same seed): it stops for every interruption and, playing an over-eager agent, for
|
|
6
|
+
half of the backchannels and coughs, a known delay after it detects the caller.
|
|
7
|
+
|
|
8
|
+
Measured, per detector and timing method, from the caller pcap alone: the agent's speech in
|
|
9
|
+
progress when the caller's interjection starts (its labelled onset), or paused for less than 1
|
|
10
|
+
s, ends at the last detected sample before a silence of at least 1 s. The agent stopped if that
|
|
11
|
+
end comes within 2 s of the interjection; stop time is that end minus the interjection onset.
|
|
12
|
+
The gate reads a stop up to its error bar early, so a negative stop time within the bar is a
|
|
13
|
+
stop; one further back means the agent's speech, as the gate heard it, had ended before the
|
|
14
|
+
caller cut in (its reply was over, it never came back from a pause, or the gate missed quiet
|
|
15
|
+
speech), and against a target the turn is left out (`ended`). Truth: the last labelled sample of
|
|
16
|
+
the agent's reply before its cut that reached the caller.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import random
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from rtpbench import analyze, detect, speechcal
|
|
25
|
+
from rtpbench.rtp import FRAME
|
|
26
|
+
|
|
27
|
+
KINDS = ("interrupt", "backchannel", "cough")
|
|
28
|
+
GAP_NS = 1_000_000_000 # silence that ends the agent's speech
|
|
29
|
+
STOP_WINDOW_NS = 2_000_000_000
|
|
30
|
+
METHOD_DETECTOR = "webrtcvad" # the detector the per-method table uses
|
|
31
|
+
WORDS = { # what each backchannel clip says, as corpus/README.md lists them
|
|
32
|
+
"tts-ald-bc": "Ajá",
|
|
33
|
+
"tts-davefx-bc": "Vale",
|
|
34
|
+
"tts-sharvardf-bc": "Vale",
|
|
35
|
+
"tts-sharvardm-bc": "Sí, sí",
|
|
36
|
+
"tts-cori-bc": "Yeah",
|
|
37
|
+
"tts-libritts0-bc": "Okay",
|
|
38
|
+
"tts-libritts3-bc": "Uh-huh",
|
|
39
|
+
"tts-ljspeech-bc": "Uh-huh",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def plan(seed: int, n: int = 1000) -> list[dict[str, Any]]:
|
|
44
|
+
"""Per turn: what the caller says, when (s after it hears the agent start), and whether
|
|
45
|
+
and how long after detecting the caller the reference agent stops."""
|
|
46
|
+
rng = random.Random(seed + 2)
|
|
47
|
+
steps = []
|
|
48
|
+
for _ in range(n):
|
|
49
|
+
kind = rng.choices(KINDS, weights=(2, 1, 1))[0]
|
|
50
|
+
steps.append(
|
|
51
|
+
{
|
|
52
|
+
"kind": kind,
|
|
53
|
+
"at_s": rng.uniform(0.5, 2.5),
|
|
54
|
+
"stop": kind == "interrupt" or rng.random() < 0.5,
|
|
55
|
+
"stop_delay_s": rng.uniform(0.1, 1.0),
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
return steps
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _segments(st: analyze.Stream, found: list[speechcal.Span], packet: bool) -> list[list[int]]:
|
|
62
|
+
"""Detected spans as times, merged across gaps shorter than GAP_NS."""
|
|
63
|
+
segs: list[list[int]] = []
|
|
64
|
+
for a, b in found:
|
|
65
|
+
if packet:
|
|
66
|
+
on, end = st.times[a // FRAME], st.times[(b - 1) // FRAME]
|
|
67
|
+
else:
|
|
68
|
+
on, end = st.time(a, True), st.time(b - 1, False)
|
|
69
|
+
if on is None or end is None:
|
|
70
|
+
continue
|
|
71
|
+
if segs and on - segs[-1][1] < GAP_NS:
|
|
72
|
+
segs[-1][1] = max(segs[-1][1], end)
|
|
73
|
+
else:
|
|
74
|
+
segs.append([on, end])
|
|
75
|
+
return segs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def stop(segs: list[list[int]], t: int) -> int | None:
|
|
79
|
+
"""End of the agent speech in progress at t (or paused under GAP_NS), minus t; None if the
|
|
80
|
+
agent had not spoken for GAP_NS."""
|
|
81
|
+
seg = next((s for s in segs if s[0] <= t < s[1] + GAP_NS), None)
|
|
82
|
+
return None if seg is None else seg[1] - t
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ended(rec: dict[str, Any], lo: float) -> bool:
|
|
86
|
+
"""Whether the agent's speech, by the gate, ended before the interjection's onset by more
|
|
87
|
+
than `lo` ms, the low side of the gate's stop error bar."""
|
|
88
|
+
v = rec["measured_ms"]["wire/gate"]
|
|
89
|
+
return v is not None and v < lo
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def turns(run: Path) -> list[dict[str, Any]]:
|
|
93
|
+
caller_log = json.loads((run / "caller.json").read_text())
|
|
94
|
+
agent_path = run / "agent.json"
|
|
95
|
+
agent_log = json.loads(agent_path.read_text()) if agent_path.exists() else None
|
|
96
|
+
cap = analyze.load(run / "caller.pcap", caller_log["rtp_port"])
|
|
97
|
+
pt, seq0 = caller_log["pt"], caller_log["seq0"]
|
|
98
|
+
segs: dict[str, tuple[analyze.Stream, list[list[int]]]] = {}
|
|
99
|
+
for m in analyze.METHODS:
|
|
100
|
+
mode = "wire" if m == "packet" else m
|
|
101
|
+
up, down = analyze.stream(cap.up, pt, mode), analyze.stream(cap.down, pt, mode)
|
|
102
|
+
for d in detect.DETECTORS if m == "wire" else (METHOD_DETECTOR,):
|
|
103
|
+
segs[f"{m}/{d}"] = up, _segments(down, down.detect(detect.DETECT[d]), m == "packet")
|
|
104
|
+
up, down = analyze.stream(cap.up, pt), analyze.stream(cap.down, pt)
|
|
105
|
+
out = []
|
|
106
|
+
for i, c in enumerate(caller_log["turns"]):
|
|
107
|
+
if not c.get("speech"):
|
|
108
|
+
continue # the agent never replied, so there was nothing to cut into
|
|
109
|
+
rec: dict[str, Any] = {"turn": i, "kind": c["kind"], "clip": c["clip"], "measured_ms": {}}
|
|
110
|
+
if start := speechcal._truth(
|
|
111
|
+
up, [(up.index(seq0, a), up.index(seq0, b)) for a, b in c["speech"]]
|
|
112
|
+
):
|
|
113
|
+
rec["onset_ns"] = start[0] # capture time of the interjection's onset, the stop's start
|
|
114
|
+
for key, (st, s) in segs.items(): # from the first labelled sample that arrived
|
|
115
|
+
said = speechcal._truth(
|
|
116
|
+
st, [(st.index(seq0, a), st.index(seq0, b)) for a, b in c["speech"]]
|
|
117
|
+
)
|
|
118
|
+
gap = None if said is None else stop(s, said[0])
|
|
119
|
+
rec["measured_ms"][key] = None if gap is None else gap / 1e6
|
|
120
|
+
if agent_log:
|
|
121
|
+
reply = agent_log["turns"][i]
|
|
122
|
+
barge = reply.get("barge", {})
|
|
123
|
+
rec |= {"stopped": "cut" in barge, "stop_delay_ms": barge.get("stop_delay_ns", 0) / 1e6}
|
|
124
|
+
if "cut" in barge:
|
|
125
|
+
a0 = agent_log["seq0"]
|
|
126
|
+
kept = [(a, min(b, barge["cut"])) for a, b in reply["speech"] if a < barge["cut"]]
|
|
127
|
+
said = speechcal._truth(
|
|
128
|
+
up, [(up.index(seq0, a), up.index(seq0, b)) for a, b in c["speech"]]
|
|
129
|
+
)
|
|
130
|
+
heard = speechcal._truth(
|
|
131
|
+
down, [(down.index(a0, a), down.index(a0, b)) for a, b in kept]
|
|
132
|
+
)
|
|
133
|
+
if said and heard:
|
|
134
|
+
rec["truth_ms"] = (heard[1] - said[0]) / 1e6
|
|
135
|
+
out.append(rec)
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def summarize(runs: dict[str, list[Path]]) -> str:
|
|
140
|
+
"""Measured stop rate and stop time per interjection kind; with a reference agent's log
|
|
141
|
+
also the error against truth and the stopped-or-not confusion. The runs under one name (one
|
|
142
|
+
per seed) pool into one row."""
|
|
143
|
+
stat = ["mean", "p5", "p50", "p95", "max abs"]
|
|
144
|
+
measured, errors, confusion = [], [], []
|
|
145
|
+
key = f"wire/{METHOD_DETECTOR}"
|
|
146
|
+
for name, dirs in runs.items():
|
|
147
|
+
recs = []
|
|
148
|
+
for run in dirs:
|
|
149
|
+
part = turns(run)
|
|
150
|
+
(run / "turns.jsonl").write_text("".join(json.dumps(r) + "\n" for r in part))
|
|
151
|
+
recs += part
|
|
152
|
+
for d in detect.DETECTORS:
|
|
153
|
+
for kind in KINDS:
|
|
154
|
+
sel = [r for r in recs if r["kind"] == kind]
|
|
155
|
+
v = [r["measured_ms"][f"wire/{d}"] for r in sel if _stopped(r, f"wire/{d}")]
|
|
156
|
+
cells = [f"{analyze._pct(v, q):.0f}" for q in (0.05, 0.5, 0.95)] if v else ["-"] * 3
|
|
157
|
+
measured.append([name, kind, d, str(len(sel)), str(len(v)), *cells])
|
|
158
|
+
truth = [r for r in recs if "truth_ms" in r]
|
|
159
|
+
if not truth:
|
|
160
|
+
continue
|
|
161
|
+
for k in recs[0]["measured_ms"]:
|
|
162
|
+
got = [r["measured_ms"][k] - r["truth_ms"] for r in truth if _stopped(r, k)]
|
|
163
|
+
errors.append(
|
|
164
|
+
[name, *k.split("/"), f"{len(got)}/{len(truth)}", *speechcal._signed(got)]
|
|
165
|
+
)
|
|
166
|
+
for kind in KINDS:
|
|
167
|
+
for stopped in (True, False):
|
|
168
|
+
sel = [r for r in recs if r["kind"] == kind and r.get("stopped") == stopped]
|
|
169
|
+
said = sum(_stopped(r, key) for r in sel)
|
|
170
|
+
agent = "stopped" if stopped else "continued"
|
|
171
|
+
confusion.append([name, kind, agent, str(len(sel)), str(said)])
|
|
172
|
+
out = "Measured: turns, stopped, stop time p5 / p50 / p95 (ms) of those stopped, wire\n\n"
|
|
173
|
+
out += analyze._table(
|
|
174
|
+
["run", "interjection", "detector", "turns", "stopped", "p5", "p50", "p95"], measured
|
|
175
|
+
)
|
|
176
|
+
if errors:
|
|
177
|
+
out += (
|
|
178
|
+
"\nBarge-in stop time: measured - truth, ms (agent stopped, rtpbench agrees)\n\n"
|
|
179
|
+
+ analyze._table(["run", "method", "detector", "n", *stat], errors)
|
|
180
|
+
+ f"\nStopped or not: what the agent did vs what rtpbench measured ({key})\n\n"
|
|
181
|
+
+ analyze._table(
|
|
182
|
+
["run", "interjection", "agent", "turns", "measured stopped"], confusion
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _stopped(rec: dict[str, Any], key: str) -> bool:
|
|
189
|
+
v = rec["measured_ms"][key]
|
|
190
|
+
return v is not None and v < STOP_WINDOW_NS / 1e6
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def pooled(runs: dict[str, list[Path]], steps: list[dict[str, Any]] | None = None) -> str:
|
|
194
|
+
"""Barge-in per profile and interjection against a real agent, over every turn of every
|
|
195
|
+
run: how often the agent stopped (and the range of that rate over runs), its stop time by
|
|
196
|
+
the gate on the wire, with the gate's error bar for the profile and codec, and how many of
|
|
197
|
+
those ran across one of the wall-clock `steps`. Turns whose agent speech ended before the
|
|
198
|
+
interjection (`ended`) are counted apart. Then how often it stopped for each clip, over the
|
|
199
|
+
same turns."""
|
|
200
|
+
from rtpbench import cases, clock
|
|
201
|
+
|
|
202
|
+
key = "wire/gate"
|
|
203
|
+
rows = []
|
|
204
|
+
clips: dict[tuple[str, str], dict[str, list[int]]] = {} # stopped, turns per profile
|
|
205
|
+
for profile, dirs in runs.items():
|
|
206
|
+
per_run, bars, widths, before = [], set(), [], []
|
|
207
|
+
for run in dirs:
|
|
208
|
+
recs = turns(run)
|
|
209
|
+
(run / "turns.jsonl").write_text("".join(json.dumps(r) + "\n" for r in recs))
|
|
210
|
+
pt = json.loads((run / "caller.json").read_text())["pt"]
|
|
211
|
+
b = cases.bar(cases.STOP, profile, pt)
|
|
212
|
+
bars.add(b)
|
|
213
|
+
widths.append(b.hi - b.lo if b else 0)
|
|
214
|
+
lo = b.lo if b else 0
|
|
215
|
+
per_run.append([r for r in recs if not ended(r, lo)])
|
|
216
|
+
before += [r["kind"] for r in recs if ended(r, lo)]
|
|
217
|
+
for r in recs:
|
|
218
|
+
tally = clips.setdefault((r["kind"], r["clip"]), {}).setdefault(profile, [0, 0, 0])
|
|
219
|
+
if ended(r, lo):
|
|
220
|
+
tally[2] += 1
|
|
221
|
+
else:
|
|
222
|
+
tally[0] += _stopped(r, key)
|
|
223
|
+
tally[1] += 1
|
|
224
|
+
error = "; ".join(f"[{b.lo:+.1f}, {b.hi:+.1f}] {b.source}" for b in bars if b) or "-"
|
|
225
|
+
for kind in KINDS:
|
|
226
|
+
sel = [[r for r in recs if r["kind"] == kind] for recs in per_run]
|
|
227
|
+
stopped = [[r for r in s if _stopped(r, key)] for s in sel]
|
|
228
|
+
n, k = sum(map(len, sel)), sum(map(len, stopped))
|
|
229
|
+
rates = [len(t) / len(s) for s, t in zip(sel, stopped, strict=True) if s]
|
|
230
|
+
spread = f"{min(rates):.0%} .. {max(rates):.0%}" if rates else "-"
|
|
231
|
+
v = [r["measured_ms"][key] for t in stopped for r in t]
|
|
232
|
+
cells = [f"{analyze._pct(v, q):.0f}" for q in (0.05, 0.5, 0.95)] if v else ["-"] * 3
|
|
233
|
+
across = sum(
|
|
234
|
+
clock.spans(
|
|
235
|
+
r["onset_ns"],
|
|
236
|
+
r["onset_ns"] + round(r["measured_ms"][key] * 1e6),
|
|
237
|
+
steps or [],
|
|
238
|
+
w,
|
|
239
|
+
)
|
|
240
|
+
for t, w in zip(stopped, widths, strict=True)
|
|
241
|
+
for r in t
|
|
242
|
+
if "onset_ns" in r
|
|
243
|
+
)
|
|
244
|
+
row = [profile, kind, str(len(dirs)), f"{k}/{n}", spread, *cells, str(across)]
|
|
245
|
+
rows.append([*row, str(before.count(kind)), error])
|
|
246
|
+
head = ["profile", "interjection", "runs", "stopped", "run rates", "p5", "p50", "p95"]
|
|
247
|
+
head += ["across a clock step", "speech ended before (gate)"]
|
|
248
|
+
order = sorted(clips, key=lambda c: (KINDS.index(c[0]), c[1]))
|
|
249
|
+
per_clip = [
|
|
250
|
+
[kind, clip, WORDS.get(clip, ""), *(_share(clips[kind, clip].get(p)) for p in runs)]
|
|
251
|
+
for kind, clip in order
|
|
252
|
+
]
|
|
253
|
+
return (
|
|
254
|
+
"Barge-in: whether the agent stopped within 2 s of the interjection's onset, and its stop "
|
|
255
|
+
"time (ms, gate, wire) when it did, every turn of every run. True value = measured - "
|
|
256
|
+
"error, error within the bar (calibrated on hard cuts). Speech ended before (gate): turns "
|
|
257
|
+
"left out, the gate having heard the agent's speech end before the onset by more than "
|
|
258
|
+
"the bar.\n\n"
|
|
259
|
+
+ analyze._table([*head, "stop error bar"], rows)
|
|
260
|
+
+ "\nPer clip: stopped / turns, over the same turns, and those left out as ended before; "
|
|
261
|
+
"interruptions are the caller's lines (--lines), backchannels and coughs the corpus's.\n\n"
|
|
262
|
+
+ analyze._table(["interjection", "clip", "words", *runs], per_clip)
|
|
263
|
+
+ "\nEach turn: a line of PROFILE/REPEAT/barge-in/turns.jsonl (results.json holds "
|
|
264
|
+
"the cases only; rtpbench check gathers the turns into check.json).\n"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _share(n: list[int] | None) -> str:
|
|
269
|
+
"""Stopped / turns, and the turns left out as ended before, if any."""
|
|
270
|
+
if not n:
|
|
271
|
+
return "-"
|
|
272
|
+
return f"{n[0]}/{n[1]}" + (f", {n[2]} ended before" if n[2] else "")
|