Spaces:
Running on Zero
Running on Zero
File size: 5,209 Bytes
875e4af | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """
Evaluation metrics shared by both baselines (and, later, any Whisper-based
model), so every experiment in experiments/EXPERIMENTS.md is measured the
same way.
Definitions used throughout this project (stated once, here, so every doc
and script agrees):
- Positive class = END (endpoint_bool = True / turn complete).
- False END = model predicted END but the true label was CONTINUE.
This is a "premature endpoint" — the costly error in a live
voice agent (interrupts the user).
- False CONTINUE = model predicted CONTINUE but the true label was END.
This is a "delayed endpoint" — the agent waits too long
before responding.
True wall-clock endpoint latency cannot be computed from this dataset (no
conversation-level timestamps — docs/INITIAL_ANALYSIS.md §7). Anywhere that
number would normally go, code in this module reports
`"endpoint_latency_ms": None` with a reason string, rather than a fabricated
number or a silently missing field.
"""
from __future__ import annotations
import time
from typing import Callable
import numpy as np
from sklearn.metrics import confusion_matrix, precision_recall_fscore_support
def classification_metrics(y_true: np.ndarray, y_pred: np.ndarray) -> dict:
y_true = np.asarray(y_true, dtype=bool)
y_pred = np.asarray(y_pred, dtype=bool)
if len(y_true) == 0:
return {"n": 0, "note": "empty evaluation set"}
accuracy = float(np.mean(y_true == y_pred))
precision, recall, f1, _ = precision_recall_fscore_support(
y_true, y_pred, average="binary", zero_division=0
)
cm = confusion_matrix(y_true, y_pred, labels=[False, True])
tn, fp, fn, tp = cm.ravel()
n_actual_continue = tn + fp # true label = CONTINUE
n_actual_end = fn + tp # true label = END
false_end_rate = float(fp / n_actual_continue) if n_actual_continue > 0 else None
false_continue_rate = float(fn / n_actual_end) if n_actual_end > 0 else None
return {
"n": int(len(y_true)),
"accuracy": accuracy,
"precision": float(precision),
"recall": float(recall),
"f1": float(f1),
"confusion_matrix": {"tn": int(tn), "fp": int(fp), "fn": int(fn), "tp": int(tp)},
"false_end_rate": false_end_rate, # premature endpoint rate
"false_continue_rate": false_continue_rate, # delayed endpoint rate
"endpoint_latency_ms": None,
"endpoint_latency_note": (
"Not measurable: dataset has no conversation-level timestamps "
"(see docs/INITIAL_ANALYSIS.md §7)."
),
}
def slice_metrics(
y_true: np.ndarray,
y_pred: np.ndarray,
slice_labels: np.ndarray,
min_samples: int = 20,
) -> dict:
"""Per-slice metrics (e.g. by language, by synthetic flag). Slices with
fewer than `min_samples` are excluded and reported as such, per the
Phase 2 brief's "only report slices with sufficient samples" rule —
exclusion is explicit, not silent.
"""
y_true = np.asarray(y_true, dtype=bool)
y_pred = np.asarray(y_pred, dtype=bool)
slice_labels = np.asarray(slice_labels)
results = {}
excluded = []
for val in sorted(set(slice_labels.tolist()), key=str):
mask = slice_labels == val
n = int(mask.sum())
if n < min_samples:
excluded.append({"slice": str(val), "n": n})
continue
results[str(val)] = classification_metrics(y_true[mask], y_pred[mask])
return {"slices": results, "excluded_insufficient_n": excluded, "min_samples": min_samples}
def measure_inference_latency(
predict_fn: Callable,
inputs: list,
n_warmup: int = 3,
n_repeats: int = 1,
) -> dict:
"""Wall-clock latency per single-item inference call, on THIS machine
(CPU, exact spec unstated — see caveat below). Reported per-call, not
batched, since production turn-detection is a per-utterance call.
Honesty caveat this function always attaches: measured latency is
environment-specific (CPU model, load, Python/library versions all
matter). It should be reported as "measured on this dev/eval machine",
never presented as a universal number, and never compared directly to
a different environment's numbers (e.g. the upstream Smart Turn
project's reported ~65ms Pipecat Cloud figure) without noting that
caveat explicitly in the write-up.
"""
for x in inputs[:n_warmup]:
predict_fn(x)
times_ms = []
for _ in range(n_repeats):
for x in inputs:
t0 = time.perf_counter()
predict_fn(x)
t1 = time.perf_counter()
times_ms.append((t1 - t0) * 1000.0)
times_ms = np.array(times_ms)
return {
"n_calls": int(len(times_ms)),
"mean_ms": float(np.mean(times_ms)),
"median_ms": float(np.median(times_ms)),
"p95_ms": float(np.percentile(times_ms, 95)),
"p99_ms": float(np.percentile(times_ms, 99)),
"min_ms": float(np.min(times_ms)),
"max_ms": float(np.max(times_ms)),
"caveat": "Measured on this dev/eval machine; not directly comparable across environments.",
}
|